feat: кастомные обои чата (SVG-паттерны, темы, глобальный фон, прозрачные панели)
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M18 20c0-4 3-7 7-7h20c4 0 7 3 7 7v8c0 4-3 7-7 7H33l-8 6 1-6h-1c-4 0-7-3-7-7z"/>
|
||||||
|
<path d="M74 58c0-3.5 3-6.5 6.5-6.5H98c3.5 0 6.5 3 6.5 6.5v7c0 3.5-3 6.5-6.5 6.5H86l-7 5 1-5c-3.5 0-6-3-6-6.5z"/>
|
||||||
|
<path d="M22 82c0-3.5 3-6.5 6.5-6.5H46c3.5 0 6.5 3 6.5 6.5v7c0 3.5-3 6.5-6.5 6.5H34l-7 5 1-5c-3.5 0-6-3-6-6.5z"/>
|
||||||
|
<circle cx="92" cy="24" r="4"/>
|
||||||
|
<circle cx="104" cy="34" r="2.5"/>
|
||||||
|
<circle cx="10" cy="56" r="2.5"/>
|
||||||
|
<circle cx="64" cy="104" r="3.5"/>
|
||||||
|
<circle cx="80" cy="110" r="2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 653 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M30 18c-3-5-11-5-13 1-2 5 2 10 13 18 11-8 15-13 13-18-2-6-10-6-13-1z"/>
|
||||||
|
<path d="M92 46c-2.4-4-8.8-4-10.4.8-1.6 4 1.6 8 10.4 14.4 8.8-6.4 12-10.4 10.4-14.4-1.6-4.8-8-4.8-10.4-.8z"/>
|
||||||
|
<path d="M34 78c-2.4-4-8.8-4-10.4.8-1.6 4 1.6 8 10.4 14.4 8.8-6.4 12-10.4 10.4-14.4-1.6-4.8-8-4.8-10.4-.8z"/>
|
||||||
|
<circle cx="70" cy="96" r="3"/>
|
||||||
|
<circle cx="102" cy="94" r="2.5"/>
|
||||||
|
<circle cx="58" cy="52" r="2.5"/>
|
||||||
|
<circle cx="10" cy="44" r="2.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 599 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M14 34 46 20 34 52l-6-9-8 6z"/>
|
||||||
|
<path d="M78 78 106 66 96 92l-5-7-6 5z"/>
|
||||||
|
<path d="M30 92 52 82 44 102l-4-6-5 4z"/>
|
||||||
|
<circle cx="88" cy="30" r="3"/>
|
||||||
|
<circle cx="104" cy="42" r="2"/>
|
||||||
|
<circle cx="60" cy="58" r="2.5"/>
|
||||||
|
<circle cx="12" cy="70" r="2.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60" fill="none">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M28 12h4v7h7v4h-7v7h-4v-7h-7v-4h7z"/>
|
||||||
|
<circle cx="0" cy="0" r="2"/>
|
||||||
|
<circle cx="60" cy="0" r="2"/>
|
||||||
|
<circle cx="0" cy="60" r="2"/>
|
||||||
|
<circle cx="60" cy="60" r="2"/>
|
||||||
|
<circle cx="30" cy="0" r="2"/>
|
||||||
|
<circle cx="0" cy="30" r="2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 392 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
|
<g fill="none" stroke="#ffffff" stroke-width="3">
|
||||||
|
<circle cx="30" cy="30" r="16"/>
|
||||||
|
<circle cx="90" cy="66" r="12"/>
|
||||||
|
<circle cx="24" cy="88" r="10"/>
|
||||||
|
<circle cx="96" cy="16" r="7"/>
|
||||||
|
<circle cx="60" cy="60" r="4"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<circle cx="66" cy="102" r="3"/>
|
||||||
|
<circle cx="6" cy="54" r="2.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 451 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M28 14c1.5 8 3 9.5 11 11-8 1.5-9.5 3-11 11-1.5-8-3-9.5-11-11 8-1.5 9.5-3 11-11z"/>
|
||||||
|
<path d="M90 50c1.2 6.4 2.4 7.6 8.8 8.8-6.4 1.2-7.6 2.4-8.8 8.8-1.2-6.4-2.4-7.6-8.8-8.8 6.4-1.2 7.6-2.4 8.8-8.8z"/>
|
||||||
|
<path d="M40 82c1 5.6 2 6.6 7.6 7.6-5.6 1-6.6 2-7.6 7.6-1-5.6-2-6.6-7.6-7.6 5.6-1 6.6-2 7.6-7.6z"/>
|
||||||
|
<circle cx="70" cy="22" r="2.5"/>
|
||||||
|
<circle cx="104" cy="30" r="2"/>
|
||||||
|
<circle cx="12" cy="60" r="2"/>
|
||||||
|
<circle cx="66" cy="104" r="2.5"/>
|
||||||
|
<circle cx="100" cy="98" r="2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 642 B |
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
|
|
||||||
import 'persisted_setting.dart';
|
import 'persisted_setting.dart';
|
||||||
|
|
||||||
enum ChatChromeStyle { color, blur, none }
|
enum ChatChromeStyle { color, blur, none, transparent }
|
||||||
|
|
||||||
class AppChatChrome {
|
class AppChatChrome {
|
||||||
static const prefKey = 'app_chat_chrome';
|
static const prefKey = 'app_chat_chrome';
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'persisted_setting.dart';
|
||||||
|
|
||||||
|
class AppWallpaperTint {
|
||||||
|
static const prefKey = 'app_wallpaper_tint';
|
||||||
|
|
||||||
|
static final _setting = PersistedSetting<bool>(
|
||||||
|
prefKey: prefKey,
|
||||||
|
defaultValue: false,
|
||||||
|
read: (prefs, key) => prefs.getBool(key),
|
||||||
|
write: (prefs, key, value) async {
|
||||||
|
await prefs.setBool(key, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
static ValueNotifier<bool> get current => _setting.current;
|
||||||
|
|
||||||
|
static Future<bool> load() => _setting.load();
|
||||||
|
|
||||||
|
static Future<void> save(bool value) => _setting.save(value);
|
||||||
|
}
|
||||||
@@ -1,31 +1,164 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../utils/tiled_svg.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ChatWallpaperTheme {
|
class ChatWallpaperTheme {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final Gradient gradient;
|
final List<Color> colors;
|
||||||
final Color bubbleTint;
|
final AlignmentGeometry begin;
|
||||||
|
final AlignmentGeometry end;
|
||||||
|
final String? pattern;
|
||||||
|
final Color patternColor;
|
||||||
|
final double patternOpacity;
|
||||||
|
final double tileSize;
|
||||||
|
final bool dark;
|
||||||
|
|
||||||
const ChatWallpaperTheme({
|
const ChatWallpaperTheme({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.gradient,
|
required this.colors,
|
||||||
this.bubbleTint = Colors.transparent,
|
this.begin = Alignment.topLeft,
|
||||||
|
this.end = Alignment.bottomRight,
|
||||||
|
this.pattern,
|
||||||
|
this.patternColor = Colors.white,
|
||||||
|
this.patternOpacity = 0.1,
|
||||||
|
this.tileSize = 130,
|
||||||
|
this.dark = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget buildBackground() => DecoratedBox(
|
Gradient get gradient =>
|
||||||
decoration: BoxDecoration(gradient: gradient),
|
LinearGradient(colors: colors, begin: begin, end: end);
|
||||||
child: const SizedBox.expand(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget buildPreview() => DecoratedBox(
|
Color get bubbleTint => colors.first;
|
||||||
decoration: BoxDecoration(gradient: gradient),
|
|
||||||
child: const SizedBox.expand(),
|
Widget buildBackground() => _ChatWallpaperThemeView(theme: this);
|
||||||
);
|
|
||||||
|
Widget buildPreview() =>
|
||||||
|
_ChatWallpaperThemeView(theme: this, tileScale: 0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
const List<ChatWallpaperTheme> kChatWallpaperThemes = <ChatWallpaperTheme>[];
|
class _ChatWallpaperThemeView extends StatelessWidget {
|
||||||
|
final ChatWallpaperTheme theme;
|
||||||
|
final double tileScale;
|
||||||
|
|
||||||
|
const _ChatWallpaperThemeView({required this.theme, this.tileScale = 1});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
DecoratedBox(decoration: BoxDecoration(gradient: theme.gradient)),
|
||||||
|
if (theme.pattern != null)
|
||||||
|
TiledSvgPattern(
|
||||||
|
asset: theme.pattern!,
|
||||||
|
color: theme.patternColor,
|
||||||
|
opacity: theme.patternOpacity,
|
||||||
|
tileSize: theme.tileSize * tileScale,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const String _kPatternDir = 'assets/wallpapers/patterns';
|
||||||
|
|
||||||
|
const List<ChatWallpaperTheme> kChatWallpaperThemes = <ChatWallpaperTheme>[
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'ocean',
|
||||||
|
name: 'Океан',
|
||||||
|
colors: [Color(0xFF2A7B9B), Color(0xFF57C1EB), Color(0xFF246FA8)],
|
||||||
|
pattern: '$_kPatternDir/bubbles.svg',
|
||||||
|
patternOpacity: 0.1,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'sunset',
|
||||||
|
name: 'Закат',
|
||||||
|
colors: [Color(0xFFFF7E5F), Color(0xFFFEB47B)],
|
||||||
|
pattern: '$_kPatternDir/hearts.svg',
|
||||||
|
patternOpacity: 0.12,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'lavender',
|
||||||
|
name: 'Лаванда',
|
||||||
|
colors: [Color(0xFF9D50BB), Color(0xFF6E48AA)],
|
||||||
|
pattern: '$_kPatternDir/stars.svg',
|
||||||
|
patternOpacity: 0.11,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'mint',
|
||||||
|
name: 'Мята',
|
||||||
|
colors: [Color(0xFF43E97B), Color(0xFF38F9D7)],
|
||||||
|
pattern: '$_kPatternDir/plus.svg',
|
||||||
|
patternColor: Colors.black,
|
||||||
|
patternOpacity: 0.06,
|
||||||
|
tileSize: 66,
|
||||||
|
dark: false,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'graphite',
|
||||||
|
name: 'Графит',
|
||||||
|
colors: [Color(0xFF232526), Color(0xFF414345)],
|
||||||
|
pattern: '$_kPatternDir/plus.svg',
|
||||||
|
patternOpacity: 0.06,
|
||||||
|
tileSize: 66,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'sky',
|
||||||
|
name: 'Небо',
|
||||||
|
colors: [Color(0xFF2193B0), Color(0xFF6DD5ED)],
|
||||||
|
pattern: '$_kPatternDir/planes.svg',
|
||||||
|
patternOpacity: 0.11,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'peach',
|
||||||
|
name: 'Персик',
|
||||||
|
colors: [Color(0xFFFFD3A5), Color(0xFFFD6585)],
|
||||||
|
pattern: '$_kPatternDir/rings.svg',
|
||||||
|
patternColor: Colors.black,
|
||||||
|
patternOpacity: 0.05,
|
||||||
|
dark: false,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'forest',
|
||||||
|
name: 'Лес',
|
||||||
|
colors: [Color(0xFF134E5E), Color(0xFF71B280)],
|
||||||
|
pattern: '$_kPatternDir/rings.svg',
|
||||||
|
patternOpacity: 0.09,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'grape',
|
||||||
|
name: 'Виноград',
|
||||||
|
colors: [Color(0xFF4776E6), Color(0xFF8E54E9)],
|
||||||
|
pattern: '$_kPatternDir/stars.svg',
|
||||||
|
patternOpacity: 0.11,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'night',
|
||||||
|
name: 'Ночь',
|
||||||
|
colors: [Color(0xFF0F2027), Color(0xFF203A43), Color(0xFF2C5364)],
|
||||||
|
pattern: '$_kPatternDir/stars.svg',
|
||||||
|
patternOpacity: 0.08,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'rose',
|
||||||
|
name: 'Роза',
|
||||||
|
colors: [Color(0xFFF4C4F3), Color(0xFFFC67FA)],
|
||||||
|
pattern: '$_kPatternDir/hearts.svg',
|
||||||
|
patternOpacity: 0.14,
|
||||||
|
),
|
||||||
|
ChatWallpaperTheme(
|
||||||
|
id: 'amber',
|
||||||
|
name: 'Янтарь',
|
||||||
|
colors: [Color(0xFFF7971E), Color(0xFFFFD200)],
|
||||||
|
pattern: '$_kPatternDir/bubbles.svg',
|
||||||
|
patternColor: Colors.black,
|
||||||
|
patternOpacity: 0.05,
|
||||||
|
dark: false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
ChatWallpaperTheme? chatWallpaperThemeById(String? id) {
|
ChatWallpaperTheme? chatWallpaperThemeById(String? id) {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import '../utils/logger.dart';
|
import '../utils/logger.dart';
|
||||||
import 'per_chat_json_store.dart';
|
import 'per_chat_json_store.dart';
|
||||||
|
|
||||||
|
const int kGlobalWallpaperChatId = 0;
|
||||||
|
|
||||||
enum ChatWallpaperKind { image, theme }
|
enum ChatWallpaperKind { image, theme }
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -445,6 +445,15 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
CachedChat? chat;
|
CachedChat? chat;
|
||||||
bool _peerIsBot = false;
|
bool _peerIsBot = false;
|
||||||
ChatWallpaper? _wallpaper;
|
ChatWallpaper? _wallpaper;
|
||||||
|
|
||||||
|
ChatChromeStyle get _effectiveChrome {
|
||||||
|
final chrome = AppChatChrome.current.value;
|
||||||
|
if (_wallpaper != null && chrome == ChatChromeStyle.none) {
|
||||||
|
return ChatChromeStyle.blur;
|
||||||
|
}
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
final ValueNotifier<double> _composerHeight = ValueNotifier(96);
|
final ValueNotifier<double> _composerHeight = ValueNotifier(96);
|
||||||
final ValueNotifier<double> _pinnedBannerHeight = ValueNotifier(0);
|
final ValueNotifier<double> _pinnedBannerHeight = ValueNotifier(0);
|
||||||
|
|
||||||
@@ -1341,6 +1350,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
.listenable(widget.chatId)
|
.listenable(widget.chatId)
|
||||||
.removeListener(_recomputeHeaderStatus);
|
.removeListener(_recomputeHeaderStatus);
|
||||||
PresenceFetch.revision.removeListener(_onPresenceChanged);
|
PresenceFetch.revision.removeListener(_onPresenceChanged);
|
||||||
|
if (_wallpaperListening) {
|
||||||
|
ChatWallpaperStore.instance.revision
|
||||||
|
.removeListener(_applyEffectiveWallpaper);
|
||||||
|
}
|
||||||
_headerStatusNotifier.dispose();
|
_headerStatusNotifier.dispose();
|
||||||
_otherReadTime.dispose();
|
_otherReadTime.dispose();
|
||||||
_chatController.dispose();
|
_chatController.dispose();
|
||||||
@@ -1893,7 +1906,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
Widget wrapChrome(Widget child) {
|
Widget wrapChrome(Widget child) {
|
||||||
if (AppChatChrome.current.value != ChatChromeStyle.blur) return child;
|
if (_effectiveChrome != ChatChromeStyle.blur) return child;
|
||||||
return _FrostedPanel(
|
return _FrostedPanel(
|
||||||
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55),
|
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55),
|
||||||
border: Border(
|
border: Border(
|
||||||
@@ -2255,7 +2268,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final height = glossy
|
final height = glossy
|
||||||
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
|
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
|
||||||
: kToolbarHeight;
|
: kToolbarHeight;
|
||||||
final chrome = AppChatChrome.current.value;
|
final chrome = _effectiveChrome;
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: chrome == ChatChromeStyle.color
|
backgroundColor: chrome == ChatChromeStyle.color
|
||||||
? (glossy ? Colors.transparent : cs.surfaceContainerHigh)
|
? (glossy ? Colors.transparent : cs.surfaceContainerHigh)
|
||||||
@@ -2289,6 +2302,24 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
: (chrome == ChatChromeStyle.transparent && !glossy)
|
||||||
|
? IgnorePointer(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
cs.surface.withValues(alpha: 0.72),
|
||||||
|
cs.surface.withValues(alpha: 0.45),
|
||||||
|
cs.surface.withValues(alpha: 0.0),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.62, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
foregroundColor: cs.onSurface,
|
foregroundColor: cs.onSurface,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
@@ -2474,11 +2505,24 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _wallpaperListening = false;
|
||||||
|
|
||||||
Future<void> _loadWallpaper() async {
|
Future<void> _loadWallpaper() async {
|
||||||
await ChatWallpaperStore.instance.load();
|
await ChatWallpaperStore.instance.load();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final wp = ChatWallpaperStore.instance.get(_myId, widget.chatId);
|
if (!_wallpaperListening) {
|
||||||
if (wp != _wallpaper) setState(() => _wallpaper = wp);
|
_wallpaperListening = true;
|
||||||
|
ChatWallpaperStore.instance.revision.addListener(_applyEffectiveWallpaper);
|
||||||
|
}
|
||||||
|
_applyEffectiveWallpaper();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyEffectiveWallpaper() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final store = ChatWallpaperStore.instance;
|
||||||
|
final wp = store.get(_myId, widget.chatId) ??
|
||||||
|
store.get(_myId, kGlobalWallpaperChatId);
|
||||||
|
if (!identical(wp, _wallpaper)) setState(() => _wallpaper = wp);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openWallpaperSheet() async {
|
Future<void> _openWallpaperSheet() async {
|
||||||
@@ -2489,13 +2533,13 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
switch (pick.type) {
|
switch (pick.type) {
|
||||||
case WallpaperPickType.none:
|
case WallpaperPickType.none:
|
||||||
await store.clear(_myId, widget.chatId);
|
await store.clear(_myId, widget.chatId);
|
||||||
if (mounted) setState(() => _wallpaper = null);
|
_applyEffectiveWallpaper();
|
||||||
break;
|
break;
|
||||||
case WallpaperPickType.theme:
|
case WallpaperPickType.theme:
|
||||||
final theme = pick.theme;
|
final theme = pick.theme;
|
||||||
if (theme == null) break;
|
if (theme == null) break;
|
||||||
final wp = await store.setTheme(_myId, widget.chatId, theme.id);
|
await store.setTheme(_myId, widget.chatId, theme.id);
|
||||||
if (mounted) setState(() => _wallpaper = wp);
|
_applyEffectiveWallpaper();
|
||||||
break;
|
break;
|
||||||
case WallpaperPickType.gallery:
|
case WallpaperPickType.gallery:
|
||||||
await _pickWallpaperFromGallery();
|
await _pickWallpaperFromGallery();
|
||||||
@@ -2532,7 +2576,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
showCustomNotification(context, 'Не удалось сохранить обои');
|
showCustomNotification(context, 'Не удалось сохранить обои');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _wallpaper = wp);
|
_applyEffectiveWallpaper();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _clearHistory() async {
|
Future<void> _clearHistory() async {
|
||||||
@@ -3896,7 +3940,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
? _prank.pinkTheme(Theme.of(context))
|
? _prank.pinkTheme(Theme.of(context))
|
||||||
: Theme.of(context);
|
: Theme.of(context);
|
||||||
final cs = theme.colorScheme;
|
final cs = theme.colorScheme;
|
||||||
final underlap = AppChatChrome.current.value != ChatChromeStyle.color;
|
final underlap = _effectiveChrome != ChatChromeStyle.color;
|
||||||
|
|
||||||
// TODO: Локализация
|
// TODO: Локализация
|
||||||
// TODO: Cклонения
|
// TODO: Cклонения
|
||||||
@@ -4018,7 +4062,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
|
|
||||||
Widget _buildUnderlapBody() {
|
Widget _buildUnderlapBody() {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
final vignette = AppChatChrome.current.value == ChatChromeStyle.none;
|
final vignette = _effectiveChrome == ChatChromeStyle.none;
|
||||||
final bannerTop = _pinnedBannerTop();
|
final bannerTop = _pinnedBannerTop();
|
||||||
final banner = _buildPinnedBanner(floating: true);
|
final banner = _buildPinnedBanner(floating: true);
|
||||||
if (banner == null) _resetPinnedBannerHeight();
|
if (banner == null) _resetPinnedBannerHeight();
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ class _ChatChromeCard extends StatelessWidget {
|
|||||||
value: ChatChromeStyle.none,
|
value: ChatChromeStyle.none,
|
||||||
label: Text(l10n.appearanceChatChromeNone),
|
label: Text(l10n.appearanceChatChromeNone),
|
||||||
),
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: ChatChromeStyle.transparent,
|
||||||
|
label: Text(l10n.appearanceChatChromeTransparent),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selected: {current},
|
selected: {current},
|
||||||
onSelectionChanged: (set) {
|
onSelectionChanged: (set) {
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../core/config/app_wallpaper_tint.dart';
|
||||||
|
import '../../../core/storage/app_database.dart';
|
||||||
|
import '../../../core/storage/chat_wallpaper_store.dart';
|
||||||
|
import '../../widgets/chat_wallpaper_sheet.dart';
|
||||||
|
import '../../widgets/chat_wallpaper_view.dart';
|
||||||
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../chats/chat_wallpaper_preview_screen.dart';
|
||||||
|
|
||||||
|
class ChatBackgroundScreen extends StatefulWidget {
|
||||||
|
const ChatBackgroundScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatBackgroundScreen> createState() => _ChatBackgroundScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatBackgroundScreenState extends State<ChatBackgroundScreen> {
|
||||||
|
int _accountId = 0;
|
||||||
|
ChatWallpaper? _wallpaper;
|
||||||
|
bool _ready = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
await ChatWallpaperStore.instance.load();
|
||||||
|
final profile = await AppDatabase.loadActiveProfile();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_accountId = profile?.id ?? 0;
|
||||||
|
_wallpaper = ChatWallpaperStore.instance
|
||||||
|
.get(_accountId, kGlobalWallpaperChatId);
|
||||||
|
_ready = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refresh() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_wallpaper = ChatWallpaperStore.instance
|
||||||
|
.get(_accountId, kGlobalWallpaperChatId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openPicker() async {
|
||||||
|
if (_accountId == 0) return;
|
||||||
|
final pick = await showChatWallpaperSheet(context, current: _wallpaper);
|
||||||
|
if (pick == null || !mounted) return;
|
||||||
|
final store = ChatWallpaperStore.instance;
|
||||||
|
switch (pick.type) {
|
||||||
|
case WallpaperPickType.none:
|
||||||
|
await store.clear(_accountId, kGlobalWallpaperChatId);
|
||||||
|
_refresh();
|
||||||
|
break;
|
||||||
|
case WallpaperPickType.theme:
|
||||||
|
final theme = pick.theme;
|
||||||
|
if (theme == null) break;
|
||||||
|
await store.setTheme(_accountId, kGlobalWallpaperChatId, theme.id);
|
||||||
|
_refresh();
|
||||||
|
break;
|
||||||
|
case WallpaperPickType.gallery:
|
||||||
|
await _pickFromGallery();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFromGallery() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.image,
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
if (result == null || result.files.isEmpty) return;
|
||||||
|
final bytes = result.files.first.bytes;
|
||||||
|
if (bytes == null) {
|
||||||
|
if (mounted) showCustomNotification(context, 'Не удалось прочитать файл');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
final settings = await Navigator.of(context).push<WallpaperImageSettings>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ChatWallpaperPreviewScreen(imageBytes: bytes),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (settings == null || !mounted) return;
|
||||||
|
final wp = await ChatWallpaperStore.instance.setImage(
|
||||||
|
_accountId,
|
||||||
|
kGlobalWallpaperChatId,
|
||||||
|
bytes,
|
||||||
|
settings: settings,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (wp == null) {
|
||||||
|
showCustomNotification(context, 'Не удалось сохранить обои');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
title: const Text(
|
||||||
|
'Фон чатов',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _preview(cs)),
|
||||||
|
_panel(cs),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _preview(ColorScheme cs) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (_wallpaper != null)
|
||||||
|
ChatWallpaperView(wallpaper: _wallpaper!)
|
||||||
|
else
|
||||||
|
ColoredBox(color: cs.surfaceContainerHighest),
|
||||||
|
_SampleBubbles(cs: cs),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _panel(ColorScheme cs) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Эти обои применяются ко всем чатам, где не выбран свой фон.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 14,
|
||||||
|
height: 1.35,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: AppWallpaperTint.current,
|
||||||
|
builder: (context, enabled, _) => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Подстраивать интерфейс под обои',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'Акцентный цвет приложения возьмётся из фона',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 12.5,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Switch(
|
||||||
|
value: enabled,
|
||||||
|
onChanged: (v) => AppWallpaperTint.save(v),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _ready ? _openPicker : null,
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Выбрать обои',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onPrimary,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SampleBubbles extends StatelessWidget {
|
||||||
|
final ColorScheme cs;
|
||||||
|
|
||||||
|
const _SampleBubbles({required this.cs});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_bubble(
|
||||||
|
text: 'Единый фон для всех чатов',
|
||||||
|
color: cs.surfaceContainerHighest.withValues(alpha: 0.94),
|
||||||
|
textColor: cs.onSurface,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_bubble(
|
||||||
|
text: 'Красиво ✨',
|
||||||
|
color: cs.primary,
|
||||||
|
textColor: cs.onPrimary,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _bubble({
|
||||||
|
required String text,
|
||||||
|
required Color color,
|
||||||
|
required Color textColor,
|
||||||
|
required Alignment alignment,
|
||||||
|
}) {
|
||||||
|
return Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 260),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import '../../../core/utils/haptics.dart';
|
|||||||
import '../../widgets/glossy_pill.dart';
|
import '../../widgets/glossy_pill.dart';
|
||||||
import 'app_icon_screen.dart';
|
import 'app_icon_screen.dart';
|
||||||
import 'appearance_screen.dart';
|
import 'appearance_screen.dart';
|
||||||
|
import 'chat_background_screen.dart';
|
||||||
import 'font_settings_screen.dart';
|
import 'font_settings_screen.dart';
|
||||||
import 'message_actions_screen.dart';
|
import 'message_actions_screen.dart';
|
||||||
import 'theme_settings_screen.dart';
|
import 'theme_settings_screen.dart';
|
||||||
@@ -41,6 +42,12 @@ class CustomizationScreen extends StatelessWidget {
|
|||||||
subtitle: 'Акцентный цвет интерфейса',
|
subtitle: 'Акцентный цвет интерфейса',
|
||||||
builder: _buildAppearance,
|
builder: _buildAppearance,
|
||||||
),
|
),
|
||||||
|
_CustomizationCategory(
|
||||||
|
icon: Symbols.wallpaper,
|
||||||
|
title: 'Фон чатов',
|
||||||
|
subtitle: 'Общие обои и темы для всех чатов',
|
||||||
|
builder: _buildChatBackground,
|
||||||
|
),
|
||||||
_CustomizationCategory(
|
_CustomizationCategory(
|
||||||
icon: Symbols.text_fields,
|
icon: Symbols.text_fields,
|
||||||
title: 'Шрифты',
|
title: 'Шрифты',
|
||||||
@@ -64,6 +71,9 @@ class CustomizationScreen extends StatelessWidget {
|
|||||||
static Widget _buildAppearance(BuildContext context) =>
|
static Widget _buildAppearance(BuildContext context) =>
|
||||||
const AppearanceScreen();
|
const AppearanceScreen();
|
||||||
|
|
||||||
|
static Widget _buildChatBackground(BuildContext context) =>
|
||||||
|
const ChatBackgroundScreen();
|
||||||
|
|
||||||
static Widget _buildFontSettings(BuildContext context) =>
|
static Widget _buildFontSettings(BuildContext context) =>
|
||||||
const FontSettingsScreen();
|
const FontSettingsScreen();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
|
|
||||||
import 'package:komet/core/config/chat_wallpaper_themes.dart';
|
import 'package:komet/core/config/chat_wallpaper_themes.dart';
|
||||||
import 'package:komet/core/storage/chat_wallpaper_store.dart';
|
import 'package:komet/core/storage/chat_wallpaper_store.dart';
|
||||||
import 'package:komet/frontend/widgets/sheet_helpers.dart';
|
|
||||||
|
|
||||||
enum WallpaperPickType { none, theme, gallery }
|
enum WallpaperPickType { none, theme, gallery }
|
||||||
|
|
||||||
@@ -24,107 +23,243 @@ Future<WallpaperPick?> showChatWallpaperSheet(
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required ChatWallpaper? current,
|
required ChatWallpaper? current,
|
||||||
}) {
|
}) {
|
||||||
return showModalBottomSheet<WallpaperPick>(
|
return Navigator.of(context).push<WallpaperPick>(
|
||||||
context: context,
|
MaterialPageRoute(
|
||||||
isScrollControlled: true,
|
fullscreenDialog: true,
|
||||||
backgroundColor: Colors.transparent,
|
builder: (_) => ChatWallpaperGalleryScreen(current: current),
|
||||||
barrierColor: Colors.black.withValues(alpha: 0.45),
|
),
|
||||||
builder: (_) => _ChatWallpaperSheet(current: current),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatWallpaperSheet extends StatelessWidget {
|
class ChatWallpaperGalleryScreen extends StatefulWidget {
|
||||||
final ChatWallpaper? current;
|
final ChatWallpaper? current;
|
||||||
|
|
||||||
const _ChatWallpaperSheet({required this.current});
|
const ChatWallpaperGalleryScreen({super.key, required this.current});
|
||||||
|
|
||||||
bool get _isNoneSelected => current == null;
|
@override
|
||||||
|
State<ChatWallpaperGalleryScreen> createState() =>
|
||||||
|
_ChatWallpaperGalleryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatWallpaperGalleryScreenState
|
||||||
|
extends State<ChatWallpaperGalleryScreen> {
|
||||||
|
ChatWallpaperTheme? _selected;
|
||||||
|
bool _isImage = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final current = widget.current;
|
||||||
|
_isImage = current?.isImage ?? false;
|
||||||
|
_selected = current == null || current.isImage
|
||||||
|
? null
|
||||||
|
: chatWallpaperThemeById(current.themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _changed {
|
||||||
|
if (_isImage) return _selected != null;
|
||||||
|
return _selected?.id != chatWallpaperThemeById(widget.current?.themeId)?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _apply() {
|
||||||
|
if (_selected == null) {
|
||||||
|
Navigator.pop(context, const WallpaperPick.none());
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context, WallpaperPick.theme(_selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
return SafeArea(
|
return Scaffold(
|
||||||
top: false,
|
backgroundColor: cs.surface,
|
||||||
child: Container(
|
appBar: AppBar(
|
||||||
decoration: BoxDecoration(
|
backgroundColor: cs.surface,
|
||||||
color: cs.surfaceContainerHigh,
|
surfaceTintColor: Colors.transparent,
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
leading: IconButton(
|
||||||
),
|
icon: const Icon(Symbols.arrow_back),
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SheetGrabber(),
|
|
||||||
_header(context, cs),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_themeRow(context, cs),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_galleryButton(context, cs),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _header(BuildContext context, ColorScheme cs) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Symbols.close, color: cs.onSurface),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
title: const Text(
|
||||||
Text(
|
'Обои',
|
||||||
'Выбрать тему',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontFamily: 'Outfit',
|
fontFamily: 'Outfit',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _preview(cs)),
|
||||||
|
_panel(cs),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _themeRow(BuildContext context, ColorScheme cs) {
|
Widget _preview(ColorScheme cs) {
|
||||||
return SizedBox(
|
final theme = _selected;
|
||||||
height: 172,
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (theme != null)
|
||||||
|
theme.buildBackground()
|
||||||
|
else
|
||||||
|
ColoredBox(color: cs.surfaceContainerHighest),
|
||||||
|
const IgnorePointer(child: _PreviewScrim()),
|
||||||
|
_SampleBubbles(theme: theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _panel(ColorScheme cs) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
SizedBox(
|
||||||
|
height: 150,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
children: [
|
children: [
|
||||||
_NoneTile(
|
_NoneTile(
|
||||||
selected: _isNoneSelected,
|
selected: _selected == null && !_isImage,
|
||||||
onTap: () => Navigator.pop(context, const WallpaperPick.none()),
|
onTap: () => setState(() {
|
||||||
|
_selected = null;
|
||||||
|
_isImage = false;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
for (final theme in kChatWallpaperThemes)
|
for (final theme in kChatWallpaperThemes)
|
||||||
_ThemeTile(
|
_ThemeTile(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
selected: current?.themeId == theme.id,
|
selected: _selected?.id == theme.id,
|
||||||
onTap: () =>
|
onTap: () => setState(() {
|
||||||
Navigator.pop(context, WallpaperPick.theme(theme)),
|
_selected = theme;
|
||||||
|
_isImage = false;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _GalleryButton(
|
||||||
|
onTap: () =>
|
||||||
|
Navigator.pop(context, const WallpaperPick.gallery()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: _ApplyButton(enabled: _changed, onTap: _apply)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewScrim extends StatelessWidget {
|
||||||
|
const _PreviewScrim();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0x14000000), Color(0x00000000), Color(0x1F000000)],
|
||||||
|
stops: [0.0, 0.5, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SampleBubbles extends StatelessWidget {
|
||||||
|
final ChatWallpaperTheme? theme;
|
||||||
|
|
||||||
|
const _SampleBubbles({required this.theme});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_bubble(
|
||||||
|
text: 'Как насчёт новых обоев для этого чата?',
|
||||||
|
color: cs.surfaceContainerHighest.withValues(alpha: 0.94),
|
||||||
|
textColor: cs.onSurface,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_bubble(
|
||||||
|
text: 'Выглядит отлично 🔥',
|
||||||
|
color: cs.primary,
|
||||||
|
textColor: cs.onPrimary,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _galleryButton(BuildContext context, ColorScheme cs) {
|
Widget _bubble({
|
||||||
return TextButton(
|
required String text,
|
||||||
onPressed: () => Navigator.pop(context, const WallpaperPick.gallery()),
|
required Color color,
|
||||||
|
required Color textColor,
|
||||||
|
required Alignment alignment,
|
||||||
|
}) {
|
||||||
|
return Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 260),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Выбрать обои из галереи',
|
text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.primary,
|
color: textColor,
|
||||||
fontSize: 17,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontFamily: 'Outfit',
|
fontFamily: 'Outfit',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,11 +268,13 @@ class _TileFrame extends StatelessWidget {
|
|||||||
final bool selected;
|
final bool selected;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final String label;
|
||||||
|
|
||||||
const _TileFrame({
|
const _TileFrame({
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
required this.label,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -147,9 +284,14 @@ class _TileFrame extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: AnimatedContainer(
|
child: SizedBox(
|
||||||
|
width: 96,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 160),
|
duration: const Duration(milliseconds: 160),
|
||||||
width: 112,
|
height: 116,
|
||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -160,7 +302,44 @@ class _TileFrame extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
child: child,
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
if (selected)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(6),
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.check,
|
||||||
|
size: 16,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected ? cs.primary : cs.onSurfaceVariant,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -180,25 +359,11 @@ class _NoneTile extends StatelessWidget {
|
|||||||
return _TileFrame(
|
return _TileFrame(
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
label: 'Без обоев',
|
||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: cs.surfaceContainerHighest,
|
color: cs.surfaceContainerHighest,
|
||||||
child: Column(
|
child: const Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Icon(Symbols.block, color: Color(0xFFFF3B30), size: 34),
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Без\nтемы',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 18,
|
|
||||||
height: 1.1,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontFamily: 'Outfit',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
const Icon(Symbols.close, color: Color(0xFFFF3B30), size: 40),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -221,7 +386,82 @@ class _ThemeTile extends StatelessWidget {
|
|||||||
return _TileFrame(
|
return _TileFrame(
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
label: theme.name,
|
||||||
child: theme.buildPreview(),
|
child: theme.buildPreview(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GalleryButton extends StatelessWidget {
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _GalleryButton({required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.image, color: cs.onSurface, size: 22),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Из галереи',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ApplyButton extends StatelessWidget {
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ApplyButton({required this.enabled, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: enabled ? onTap : null,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 140),
|
||||||
|
opacity: enabled ? 1 : 0.4,
|
||||||
|
child: Container(
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.primary,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Применить',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onPrimary,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -227,9 +229,21 @@ class _StickerPanelState extends State<StickerPanel>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return SizedBox(
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
color: cs.surface,
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ui.ImageFilter.blur(sigmaX: 34, sigmaY: 34),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surface.withValues(alpha: 0.38),
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: cs.outlineVariant.withValues(alpha: 0.4),
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: _loading
|
child: _loading
|
||||||
? Center(child: SmallSpinner())
|
? Center(child: SmallSpinner())
|
||||||
: _error != null || _sections.isEmpty
|
: _error != null || _sections.isEmpty
|
||||||
@@ -276,6 +290,9 @@ class _StickerPanelState extends State<StickerPanel>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,7 @@
|
|||||||
"appearanceChatChromeColor": "Color",
|
"appearanceChatChromeColor": "Color",
|
||||||
"appearanceChatChromeBlur": "Blur",
|
"appearanceChatChromeBlur": "Blur",
|
||||||
"appearanceChatChromeNone": "None",
|
"appearanceChatChromeNone": "None",
|
||||||
|
"appearanceChatChromeTransparent": "Clear",
|
||||||
"appearanceGradientTitle": "Gradient",
|
"appearanceGradientTitle": "Gradient",
|
||||||
"appearanceGradientSubtitle": "Depth and highlights in Glossy capsules",
|
"appearanceGradientSubtitle": "Depth and highlights in Glossy capsules",
|
||||||
"appearanceAccentColorTitle": "Accent color",
|
"appearanceAccentColorTitle": "Accent color",
|
||||||
|
|||||||
@@ -1532,6 +1532,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'None'**
|
/// **'None'**
|
||||||
String get appearanceChatChromeNone;
|
String get appearanceChatChromeNone;
|
||||||
|
|
||||||
|
/// No description provided for @appearanceChatChromeTransparent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear'**
|
||||||
|
String get appearanceChatChromeTransparent;
|
||||||
|
|
||||||
/// No description provided for @appearanceGradientTitle.
|
/// No description provided for @appearanceGradientTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -762,6 +762,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appearanceChatChromeNone => 'None';
|
String get appearanceChatChromeNone => 'None';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appearanceChatChromeTransparent => 'Clear';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appearanceGradientTitle => 'Gradient';
|
String get appearanceGradientTitle => 'Gradient';
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appearanceChatChromeNone => 'Нет';
|
String get appearanceChatChromeNone => 'Нет';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appearanceChatChromeTransparent => 'Прозр.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appearanceGradientTitle => 'Градиент';
|
String get appearanceGradientTitle => 'Градиент';
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,7 @@
|
|||||||
"appearanceChatChromeColor": "Цвет",
|
"appearanceChatChromeColor": "Цвет",
|
||||||
"appearanceChatChromeBlur": "Блюр",
|
"appearanceChatChromeBlur": "Блюр",
|
||||||
"appearanceChatChromeNone": "Нет",
|
"appearanceChatChromeNone": "Нет",
|
||||||
|
"appearanceChatChromeTransparent": "Прозр.",
|
||||||
"appearanceGradientTitle": "Градиент",
|
"appearanceGradientTitle": "Градиент",
|
||||||
"appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах",
|
"appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах",
|
||||||
"appearanceAccentColorTitle": "Акцентный цвет",
|
"appearanceAccentColorTitle": "Акцентный цвет",
|
||||||
|
|||||||
+45
-3
@@ -38,6 +38,9 @@ import 'core/config/app_media_cache.dart';
|
|||||||
import 'core/config/app_pill_gradient.dart';
|
import 'core/config/app_pill_gradient.dart';
|
||||||
import 'core/config/app_visual_style.dart';
|
import 'core/config/app_visual_style.dart';
|
||||||
import 'core/config/app_chat_chrome.dart';
|
import 'core/config/app_chat_chrome.dart';
|
||||||
|
import 'core/config/app_wallpaper_tint.dart';
|
||||||
|
import 'core/storage/chat_wallpaper_store.dart';
|
||||||
|
import 'core/utils/wallpaper_seed.dart';
|
||||||
import 'core/config/app_theme_mode.dart';
|
import 'core/config/app_theme_mode.dart';
|
||||||
import 'core/config/app_theme_schedule.dart';
|
import 'core/config/app_theme_schedule.dart';
|
||||||
import 'core/config/app_digital_id_mode.dart';
|
import 'core/config/app_digital_id_mode.dart';
|
||||||
@@ -187,6 +190,7 @@ void main(List<String> args) async {
|
|||||||
final pillGradientFuture = AppPillGradient.load();
|
final pillGradientFuture = AppPillGradient.load();
|
||||||
final visualStyleFuture = AppVisualStyle.load();
|
final visualStyleFuture = AppVisualStyle.load();
|
||||||
final chatChromeFuture = AppChatChrome.load();
|
final chatChromeFuture = AppChatChrome.load();
|
||||||
|
final wallpaperTintFuture = AppWallpaperTint.load();
|
||||||
final themeScheduleFuture = AppThemeSchedule.load();
|
final themeScheduleFuture = AppThemeSchedule.load();
|
||||||
final messageActionsFuture = AppMessageActionsStyle.load();
|
final messageActionsFuture = AppMessageActionsStyle.load();
|
||||||
final swipeBackFuture = AppSwipeBackDesktop.load();
|
final swipeBackFuture = AppSwipeBackDesktop.load();
|
||||||
@@ -237,6 +241,7 @@ void main(List<String> args) async {
|
|||||||
pillGradientFuture,
|
pillGradientFuture,
|
||||||
visualStyleFuture,
|
visualStyleFuture,
|
||||||
chatChromeFuture,
|
chatChromeFuture,
|
||||||
|
wallpaperTintFuture,
|
||||||
themeScheduleFuture,
|
themeScheduleFuture,
|
||||||
messageActionsFuture,
|
messageActionsFuture,
|
||||||
swipeBackFuture,
|
swipeBackFuture,
|
||||||
@@ -310,6 +315,7 @@ class KometAppState extends State<KometApp>
|
|||||||
late final ValueNotifier<Color?> accentSeed = ValueNotifier(
|
late final ValueNotifier<Color?> accentSeed = ValueNotifier(
|
||||||
widget.initialAccentSeed,
|
widget.initialAccentSeed,
|
||||||
);
|
);
|
||||||
|
final ValueNotifier<Color?> wallpaperSeed = ValueNotifier(null);
|
||||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||||
@@ -345,8 +351,11 @@ class KometAppState extends State<KometApp>
|
|||||||
AppThemeModeConfig.current.addListener(_onThemeModeChanged);
|
AppThemeModeConfig.current.addListener(_onThemeModeChanged);
|
||||||
AppAmoled.current.addListener(_onAmoledChanged);
|
AppAmoled.current.addListener(_onAmoledChanged);
|
||||||
AppThemeSchedule.current.addListener(_onScheduleChanged);
|
AppThemeSchedule.current.addListener(_onScheduleChanged);
|
||||||
|
AppWallpaperTint.current.addListener(_onWallpaperTintChanged);
|
||||||
|
ChatWallpaperStore.instance.revision.addListener(_onWallpaperTintChanged);
|
||||||
_lastAppliedThemeMode = _effectiveThemeMode;
|
_lastAppliedThemeMode = _effectiveThemeMode;
|
||||||
_rescheduleSwitch();
|
_rescheduleSwitch();
|
||||||
|
unawaited(_refreshWallpaperSeed());
|
||||||
|
|
||||||
api.setReconnectCallback(() async {
|
api.setReconnectCallback(() async {
|
||||||
try {
|
try {
|
||||||
@@ -365,6 +374,7 @@ class KometAppState extends State<KometApp>
|
|||||||
_loginStatusSub = accountModule.loginStatusStream.listen((status) async {
|
_loginStatusSub = accountModule.loginStatusStream.listen((status) async {
|
||||||
if (status == LoginStatus.success) {
|
if (status == LoginStatus.success) {
|
||||||
DeepLinkService.instance.markReady();
|
DeepLinkService.instance.markReady();
|
||||||
|
unawaited(_refreshWallpaperSeed());
|
||||||
CallController.instance.init(api);
|
CallController.instance.init(api);
|
||||||
OutboxService.instance.init(api, messagesModule);
|
OutboxService.instance.init(api, messagesModule);
|
||||||
SelfCheckService.instance.init(api);
|
SelfCheckService.instance.init(api);
|
||||||
@@ -510,6 +520,8 @@ class KometAppState extends State<KometApp>
|
|||||||
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
||||||
AppAmoled.current.removeListener(_onAmoledChanged);
|
AppAmoled.current.removeListener(_onAmoledChanged);
|
||||||
AppThemeSchedule.current.removeListener(_onScheduleChanged);
|
AppThemeSchedule.current.removeListener(_onScheduleChanged);
|
||||||
|
AppWallpaperTint.current.removeListener(_onWallpaperTintChanged);
|
||||||
|
ChatWallpaperStore.instance.revision.removeListener(_onWallpaperTintChanged);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_profileUpdateController.close();
|
_profileUpdateController.close();
|
||||||
fpsOverlayEnabled.dispose();
|
fpsOverlayEnabled.dispose();
|
||||||
@@ -517,6 +529,7 @@ class KometAppState extends State<KometApp>
|
|||||||
tlsInsecureEnabled.dispose();
|
tlsInsecureEnabled.dispose();
|
||||||
fontScale.dispose();
|
fontScale.dispose();
|
||||||
accentSeed.dispose();
|
accentSeed.dispose();
|
||||||
|
wallpaperSeed.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,6 +732,27 @@ class KometAppState extends State<KometApp>
|
|||||||
accentSeed.value = seed;
|
accentSeed.value = seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onWallpaperTintChanged() => unawaited(_refreshWallpaperSeed());
|
||||||
|
|
||||||
|
Future<void> _refreshWallpaperSeed() async {
|
||||||
|
if (!AppWallpaperTint.current.value) {
|
||||||
|
wallpaperSeed.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final profile = await AppDatabase.loadActiveProfile();
|
||||||
|
final accountId = profile?.id ?? 0;
|
||||||
|
if (accountId == 0) {
|
||||||
|
wallpaperSeed.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ChatWallpaperStore.instance.load();
|
||||||
|
final wallpaper =
|
||||||
|
ChatWallpaperStore.instance.get(accountId, kGlobalWallpaperChatId);
|
||||||
|
final seed = await computeWallpaperSeed(wallpaper);
|
||||||
|
if (!mounted) return;
|
||||||
|
wallpaperSeed.value = seed;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> applyAppFont(String fontId) async {
|
Future<void> applyAppFont(String fontId) async {
|
||||||
if (_fontId == fontId) return;
|
if (_fontId == fontId) return;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -850,9 +884,17 @@ class KometAppState extends State<KometApp>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||||
return ValueListenableBuilder<Color?>(
|
return ListenableBuilder(
|
||||||
valueListenable: accentSeed,
|
listenable: Listenable.merge([
|
||||||
builder: (context, seed, _) {
|
accentSeed,
|
||||||
|
wallpaperSeed,
|
||||||
|
AppWallpaperTint.current,
|
||||||
|
]),
|
||||||
|
builder: (context, _) {
|
||||||
|
final seed = AppWallpaperTint.current.value &&
|
||||||
|
wallpaperSeed.value != null
|
||||||
|
? wallpaperSeed.value
|
||||||
|
: accentSeed.value;
|
||||||
final ColorScheme lightBase;
|
final ColorScheme lightBase;
|
||||||
final ColorScheme darkBase;
|
final ColorScheme darkBase;
|
||||||
if (seed != null) {
|
if (seed != null) {
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ PODS:
|
|||||||
- PromisesObjC (2.4.1)
|
- PromisesObjC (2.4.1)
|
||||||
- record_macos (1.2.1):
|
- record_macos (1.2.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -144,6 +146,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/darwin`)
|
- photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/darwin`)
|
||||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||||
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
@@ -205,6 +208,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/photo_manager/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/photo_manager/darwin
|
||||||
record_macos:
|
record_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||||
|
share_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
@@ -247,6 +252,7 @@ SPEC CHECKSUMS:
|
|||||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
||||||
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
||||||
record_macos: 5d55909f9650314be6424ffd6b123ac75a08c3c1
|
record_macos: 5d55909f9650314be6424ffd6b123ac75a08c3c1
|
||||||
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|||||||
@@ -547,6 +547,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1029,6 +1037,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1570,6 +1586,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.3"
|
version: "4.5.3"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.13"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.6"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ dependencies:
|
|||||||
media_kit_libs_macos_video: ^1.1.4
|
media_kit_libs_macos_video: ^1.1.4
|
||||||
sensors_plus: ^7.1.0
|
sensors_plus: ^7.1.0
|
||||||
smart_auth: ^3.2.0
|
smart_auth: ^3.2.0
|
||||||
|
flutter_svg: ^2.0.10
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -131,6 +132,7 @@ flutter:
|
|||||||
- assets/meteor_icon.png
|
- assets/meteor_icon.png
|
||||||
- assets/emoji_keywords.json
|
- assets/emoji_keywords.json
|
||||||
- assets/lottie/
|
- assets/lottie/
|
||||||
|
- assets/wallpapers/patterns/
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Inter
|
- family: Inter
|
||||||
|
|||||||
Reference in New Issue
Block a user