Files
Qlyra/lib/frontend/widgets/chat_wallpaper_sheet.dart
T

473 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/core/config/chat_wallpaper_themes.dart';
import 'package:komet/core/config/app_colors.dart';
import 'package:komet/core/storage/chat_wallpaper_store.dart';
import '../../core/config/app_fonts.dart';
enum WallpaperPickType { none, theme, gallery }
class WallpaperPick {
final WallpaperPickType type;
final ChatWallpaperTheme? theme;
const WallpaperPick.none() : type = WallpaperPickType.none, theme = null;
const WallpaperPick.gallery()
: type = WallpaperPickType.gallery,
theme = null;
const WallpaperPick.theme(this.theme) : type = WallpaperPickType.theme;
}
Future<WallpaperPick?> showChatWallpaperSheet(
BuildContext context, {
required ChatWallpaper? current,
}) {
return Navigator.of(context).push<WallpaperPick>(
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => ChatWallpaperGalleryScreen(current: current),
),
);
}
class ChatWallpaperGalleryScreen extends StatefulWidget {
final ChatWallpaper? current;
const ChatWallpaperGalleryScreen({super.key, required this.current});
@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
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Symbols.arrow_back),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Обои',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
fontFamily: displayFontOf(context),
),
),
),
body: Column(
children: [
Expanded(child: _preview(cs)),
_panel(cs),
],
),
);
}
Widget _preview(ColorScheme cs) {
final theme = _selected;
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(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_NoneTile(
selected: _selected == null && !_isImage,
onTap: () => setState(() {
_selected = null;
_isImage = false;
}),
),
for (final theme in kChatWallpaperThemes)
_ThemeTile(
theme: theme,
selected: _selected?.id == theme.id,
onTap: () => setState(() {
_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(
context,
text: 'Как насчёт новых обоев для этого чата?',
color: cs.surfaceContainerHighest.withValues(alpha: 0.94),
textColor: cs.onSurface,
alignment: Alignment.centerLeft,
),
const SizedBox(height: 8),
_bubble(
context,
text: 'Выглядит отлично 🔥',
color: cs.primary,
textColor: cs.onPrimary,
alignment: Alignment.centerRight,
),
],
),
),
);
}
Widget _bubble(
BuildContext context, {
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: displayFontOf(context),
),
),
),
),
);
}
}
class _TileFrame extends StatelessWidget {
final bool selected;
final VoidCallback onTap;
final Widget child;
final String label;
const _TileFrame({
required this.selected,
required this.onTap,
required this.child,
required this.label,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: onTap,
child: SizedBox(
width: 96,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 160),
height: 116,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected ? cs.primary : Colors.transparent,
width: 2.5,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
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: displayFontOf(context),
),
),
],
),
),
),
);
}
}
class _NoneTile extends StatelessWidget {
final bool selected;
final VoidCallback onTap;
const _NoneTile({required this.selected, required this.onTap});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return _TileFrame(
selected: selected,
onTap: onTap,
label: 'Без обоев',
child: ColoredBox(
color: cs.surfaceContainerHighest,
child: const Center(
child: Icon(Symbols.block, color: kDangerRed, size: 34),
),
),
);
}
}
class _ThemeTile extends StatelessWidget {
final ChatWallpaperTheme theme;
final bool selected;
final VoidCallback onTap;
const _ThemeTile({
required this.theme,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return _TileFrame(
selected: selected,
onTap: onTap,
label: theme.name,
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: displayFontOf(context),
),
),
],
),
),
);
}
}
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: displayFontOf(context),
),
),
),
),
),
);
}
}