845 lines
26 KiB
Dart
845 lines
26 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:moonwell_launcher/app/design_system/mw_primitives.dart';
|
||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||
|
||
const _ivory = Color(0xFFECE4D0);
|
||
const _muted = Color(0x80ECE4D0);
|
||
const _border = Color(0x247FB8D4);
|
||
|
||
class MwGameTile extends StatelessWidget {
|
||
const MwGameTile({
|
||
super.key,
|
||
required this.title,
|
||
required this.icon,
|
||
this.subtitle,
|
||
this.selected = false,
|
||
this.enabled = true,
|
||
this.compact = false,
|
||
this.onPressed,
|
||
this.mark,
|
||
});
|
||
|
||
final String title;
|
||
final String? subtitle;
|
||
final IconData icon;
|
||
final bool selected;
|
||
final bool enabled;
|
||
final bool compact;
|
||
final VoidCallback? onPressed;
|
||
final String? mark;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
return Opacity(
|
||
opacity: enabled ? 1 : .58,
|
||
child: Material(
|
||
color: selected ? accent.withAlpha(25) : accent.withAlpha(8),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
side: BorderSide(color: selected ? accent : _border),
|
||
),
|
||
child: InkWell(
|
||
onTap: enabled ? onPressed : null,
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: SizedBox(
|
||
height: 52,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 32,
|
||
height: 32,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [const Color(0xFF5B3A8A), accent],
|
||
),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
mark ?? title.characters.first,
|
||
style: const TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: _ivory,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
if (!compact) ...[
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title.toUpperCase(),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: _ivory,
|
||
fontSize: 10,
|
||
letterSpacing: .8,
|
||
),
|
||
),
|
||
const SizedBox(height: 3),
|
||
Text(
|
||
(subtitle ?? (enabled ? 'OPEN BETA' : 'СКОРО'))
|
||
.toUpperCase(),
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: selected ? accent : _muted,
|
||
fontSize: 8,
|
||
letterSpacing: 1.1,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (!enabled)
|
||
const Text(
|
||
'SOON',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 7,
|
||
letterSpacing: 1,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class MwGameRail extends StatelessWidget {
|
||
const MwGameRail({super.key, this.compact = false});
|
||
|
||
final bool compact;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(14, 48, 14, 30),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Color(0xB5070A18), Color(0x8F070A18)],
|
||
),
|
||
border: Border(right: BorderSide(color: _border)),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
gradient: LinearGradient(
|
||
colors: [accent, const Color(0xFF5B3A8A)],
|
||
),
|
||
border: Border.all(color: accent),
|
||
boxShadow: [
|
||
BoxShadow(color: accent.withAlpha(70), blurRadius: 12),
|
||
],
|
||
),
|
||
child: const Text('A', style: TextStyle(fontFamily: 'Cinzel')),
|
||
),
|
||
const SizedBox(height: 14),
|
||
const Divider(color: _border),
|
||
const Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Padding(
|
||
padding: EdgeInsets.fromLTRB(8, 7, 0, 8),
|
||
child: Text(
|
||
'ИГРЫ',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 9,
|
||
letterSpacing: 2.2,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
MwGameTile(
|
||
title: 'MoonWell · WoW',
|
||
subtitle: 'Open Beta',
|
||
mark: 'MW',
|
||
icon: Icons.nights_stay,
|
||
selected: true,
|
||
compact: compact,
|
||
onPressed: () {},
|
||
),
|
||
const SizedBox(height: 8),
|
||
const MwGameTile(
|
||
title: 'MoonWell Karts',
|
||
subtitle: 'Closed Alpha',
|
||
mark: 'K',
|
||
icon: Icons.sports_motorsports,
|
||
enabled: false,
|
||
),
|
||
const SizedBox(height: 8),
|
||
const MwGameTile(
|
||
title: 'Tides of Elune',
|
||
subtitle: 'В разработке',
|
||
mark: 'T',
|
||
icon: Icons.style,
|
||
enabled: false,
|
||
),
|
||
const SizedBox(height: 8),
|
||
const MwGameTile(
|
||
title: 'Project Verge',
|
||
subtitle: 'Анонс',
|
||
mark: 'V',
|
||
icon: Icons.auto_awesome,
|
||
enabled: false,
|
||
),
|
||
const Spacer(),
|
||
const Divider(color: _border),
|
||
Row(
|
||
children: [
|
||
IconButton(
|
||
onPressed: null,
|
||
tooltip: 'Друзья',
|
||
icon: const Icon(Icons.people_outline, size: 18),
|
||
),
|
||
const Spacer(),
|
||
Icon(
|
||
Icons.settings_outlined,
|
||
color: accent.withAlpha(150),
|
||
size: 18,
|
||
),
|
||
const SizedBox(width: 12),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class MwNewsItemData {
|
||
const MwNewsItemData({
|
||
required this.title,
|
||
required this.body,
|
||
this.createdAt,
|
||
this.imageUrl,
|
||
});
|
||
final String title;
|
||
final String body;
|
||
final DateTime? createdAt;
|
||
final String? imageUrl;
|
||
}
|
||
|
||
class MwNewsItem extends StatelessWidget {
|
||
const MwNewsItem({super.key, required this.item});
|
||
final MwNewsItemData item;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
final date = item.createdAt?.toLocal();
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(0, 13, 12, 13),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: accent),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
'НОВОСТЬ',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: accent,
|
||
fontSize: 8,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
date == null
|
||
? 'СЕГОДНЯ'
|
||
: '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}',
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 9,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 7),
|
||
Text(
|
||
item.title,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontFamily: 'Cormorant Garamond',
|
||
color: _ivory,
|
||
fontSize: 16,
|
||
height: 1.25,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
enum MwNewsListState { loading, empty, failure, populated }
|
||
|
||
class MwNewsList extends StatelessWidget {
|
||
const MwNewsList({
|
||
super.key,
|
||
required this.state,
|
||
this.items = const [],
|
||
this.error,
|
||
this.onRetry,
|
||
});
|
||
final MwNewsListState state;
|
||
final List<MwNewsItemData> items;
|
||
final String? error;
|
||
final VoidCallback? onRetry;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xD00A0D1F),
|
||
border: Border.all(color: const Color(0x477FB8D4)),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
children: [
|
||
SizedBox(
|
||
height: 45,
|
||
child: Row(
|
||
children: [
|
||
_NewsTab(label: 'НОВОСТИ', selected: true, accent: accent),
|
||
_NewsTab(label: 'СОБЫТИЯ', accent: accent),
|
||
_NewsTab(label: 'ПАТЧИ', accent: accent),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1, color: _border),
|
||
Expanded(child: _content()),
|
||
Container(
|
||
height: 40,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(top: BorderSide(color: _border)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text(
|
||
'MOONWELL.GG',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 9,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
'ВСЕ →',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: accent,
|
||
fontSize: 9,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _content() => switch (state) {
|
||
MwNewsListState.loading => const Center(
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
),
|
||
MwNewsListState.empty => const Center(
|
||
child: Text('Новостей пока нет', style: TextStyle(color: _muted)),
|
||
),
|
||
MwNewsListState.failure => Center(
|
||
child: Text(
|
||
error ?? 'Не удалось загрузить новости',
|
||
style: const TextStyle(color: Color(0xFFD76A78)),
|
||
),
|
||
),
|
||
MwNewsListState.populated => ListView.separated(
|
||
padding: const EdgeInsets.only(left: 16, right: 4),
|
||
itemCount: items.length,
|
||
separatorBuilder: (_, _) => const Divider(height: 1, color: _border),
|
||
itemBuilder: (_, index) => MwNewsItem(item: items[index]),
|
||
),
|
||
};
|
||
}
|
||
|
||
class _NewsTab extends StatelessWidget {
|
||
const _NewsTab({
|
||
required this.label,
|
||
required this.accent,
|
||
this.selected = false,
|
||
});
|
||
final String label;
|
||
final Color accent;
|
||
final bool selected;
|
||
@override
|
||
Widget build(BuildContext context) => Expanded(
|
||
child: Container(
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: selected ? accent.withAlpha(12) : Colors.transparent,
|
||
border: Border(
|
||
bottom: BorderSide(color: selected ? accent : Colors.transparent),
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: selected ? _ivory : _muted,
|
||
fontSize: 9,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
enum MwSyncPresentation {
|
||
install,
|
||
scanning,
|
||
downloading,
|
||
pausing,
|
||
paused,
|
||
verifying,
|
||
ready,
|
||
launched,
|
||
failure,
|
||
}
|
||
|
||
class MwSyncActionArea extends StatelessWidget {
|
||
const MwSyncActionArea({
|
||
super.key,
|
||
required this.state,
|
||
required this.status,
|
||
required this.onPrimary,
|
||
this.onPause,
|
||
this.progress,
|
||
this.currentPath,
|
||
this.error,
|
||
});
|
||
final MwSyncPresentation state;
|
||
final String status;
|
||
final VoidCallback onPrimary;
|
||
final VoidCallback? onPause;
|
||
final double? progress;
|
||
final String? currentPath;
|
||
final String? error;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
final ready = state == MwSyncPresentation.ready;
|
||
final active = {
|
||
MwSyncPresentation.scanning,
|
||
MwSyncPresentation.downloading,
|
||
MwSyncPresentation.pausing,
|
||
MwSyncPresentation.verifying,
|
||
}.contains(state);
|
||
final value = ready || state == MwSyncPresentation.launched
|
||
? 1.0
|
||
: (progress ?? 0);
|
||
final label = switch (state) {
|
||
MwSyncPresentation.install => 'УСТАНОВИТЬ',
|
||
MwSyncPresentation.paused => 'ПРОДОЛЖИТЬ',
|
||
MwSyncPresentation.ready => 'ИГРАТЬ',
|
||
MwSyncPresentation.launched => 'ИГРА ЗАПУЩЕНА',
|
||
MwSyncPresentation.failure => 'ПОВТОРИТЬ',
|
||
_ => 'ПАУЗА',
|
||
};
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(32, 18, 32, 20),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Color(0x0006091A), Color(0xF206091A)],
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0x99070A18),
|
||
border: Border.all(color: _border),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
_PulseDot(),
|
||
SizedBox(width: 12),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
"ELUNE'S GRACE",
|
||
style: TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: _ivory,
|
||
fontSize: 11,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
SizedBox(height: 3),
|
||
Text(
|
||
'ONLINE · EU',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 8,
|
||
letterSpacing: 1.1,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 24),
|
||
Expanded(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
error ?? status,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: error == null
|
||
? accent
|
||
: const Color(0xFFD76A78),
|
||
fontSize: 10,
|
||
letterSpacing: 1.1,
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'${(value * 100).round()}%',
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _ivory,
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(2),
|
||
child: LinearProgressIndicator(
|
||
value: value,
|
||
minHeight: 4,
|
||
color: accent,
|
||
backgroundColor: accent.withAlpha(28),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
currentPath ??
|
||
(ready ? 'Все файлы целы' : 'Подготовка клиента'),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 8,
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
ready ? 'Последняя проверка: только что' : '',
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 8,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 24),
|
||
SizedBox(
|
||
width: 220,
|
||
height: 64,
|
||
child: ElevatedButton.icon(
|
||
onPressed: active
|
||
? onPause
|
||
: (state == MwSyncPresentation.launched ? null : onPrimary),
|
||
icon: Icon(
|
||
ready
|
||
? Icons.play_arrow
|
||
: active
|
||
? Icons.pause
|
||
: Icons.refresh,
|
||
size: 20,
|
||
),
|
||
label: Text(label),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: ready
|
||
? const Color(0xFF60B17F)
|
||
: state == MwSyncPresentation.failure
|
||
? const Color(0xFFD76A78)
|
||
: const Color(0xFF6D499E),
|
||
foregroundColor: ready ? const Color(0xFF07130F) : _ivory,
|
||
textStyle: const TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
fontSize: 14,
|
||
letterSpacing: 2.4,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PulseDot extends StatelessWidget {
|
||
const _PulseDot();
|
||
@override
|
||
Widget build(BuildContext context) => Container(
|
||
width: 8,
|
||
height: 8,
|
||
decoration: const BoxDecoration(
|
||
color: Color(0xFF6FC28A),
|
||
shape: BoxShape.circle,
|
||
boxShadow: [BoxShadow(color: Color(0xAA6FC28A), blurRadius: 8)],
|
||
),
|
||
);
|
||
}
|
||
|
||
class MwLauncherStatusBar extends StatelessWidget {
|
||
const MwLauncherStatusBar({
|
||
super.key,
|
||
required this.status,
|
||
this.fileCount,
|
||
this.speed,
|
||
this.eta,
|
||
this.buildHash,
|
||
});
|
||
final String status;
|
||
final String? fileCount;
|
||
final String? speed;
|
||
final String? eta;
|
||
final String? buildHash;
|
||
@override
|
||
Widget build(BuildContext context) => Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||
decoration: const BoxDecoration(
|
||
color: Color(0xD904060E),
|
||
border: Border(top: BorderSide(color: _border)),
|
||
),
|
||
child: DefaultTextStyle(
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 8,
|
||
letterSpacing: .8,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||
),
|
||
if (fileCount != null) Text(fileCount!),
|
||
if (speed != null) ...[const SizedBox(width: 24), Text(speed!)],
|
||
if (eta != null) ...[const SizedBox(width: 24), Text(eta!)],
|
||
if (buildHash != null) ...[
|
||
const SizedBox(width: 24),
|
||
Text(buildHash!),
|
||
],
|
||
const SizedBox(width: 24),
|
||
const Text('Лаунчер v1.0.0'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
class MwAccountAffordance extends StatelessWidget {
|
||
const MwAccountAffordance({
|
||
super.key,
|
||
required this.onLogout,
|
||
this.onSettings,
|
||
});
|
||
final VoidCallback onLogout;
|
||
final VoidCallback? onSettings;
|
||
@override
|
||
Widget build(BuildContext context) => PopupMenuButton<String>(
|
||
tooltip: 'Аккаунт',
|
||
onSelected: (value) =>
|
||
value == 'settings' ? onSettings?.call() : onLogout(),
|
||
itemBuilder: (_) => const [
|
||
PopupMenuItem(value: 'settings', child: Text('Настройки')),
|
||
PopupMenuItem(value: 'logout', child: Text('Выйти')),
|
||
],
|
||
child: Container(
|
||
padding: const EdgeInsets.fromLTRB(8, 7, 15, 7),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0x99070A18),
|
||
border: Border.all(color: _border),
|
||
borderRadius: BorderRadius.circular(99),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
CircleAvatar(
|
||
radius: 18,
|
||
backgroundColor: Color(0xFF5B3A8A),
|
||
child: Text(
|
||
'Æ',
|
||
style: TextStyle(fontFamily: 'Cinzel', fontSize: 12),
|
||
),
|
||
),
|
||
SizedBox(width: 11),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Aranthel',
|
||
style: TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: _ivory,
|
||
fontSize: 12,
|
||
letterSpacing: 1,
|
||
),
|
||
),
|
||
SizedBox(height: 2),
|
||
Text(
|
||
'ХРАНИТЕЛЬ РОЩИ',
|
||
style: TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: _muted,
|
||
fontSize: 7,
|
||
letterSpacing: 1.1,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
class MwSettingsPanel extends StatelessWidget {
|
||
const MwSettingsPanel({
|
||
super.key,
|
||
required this.installationPath,
|
||
required this.themeVariant,
|
||
required this.onChooseDirectory,
|
||
required this.onThemeChanged,
|
||
required this.onVerify,
|
||
required this.onLogout,
|
||
this.syncActive = false,
|
||
});
|
||
final String? installationPath;
|
||
final MoonWellThemeVariant themeVariant;
|
||
final VoidCallback onChooseDirectory;
|
||
final ValueChanged<MoonWellThemeVariant> onThemeChanged;
|
||
final VoidCallback onVerify;
|
||
final VoidCallback onLogout;
|
||
final bool syncActive;
|
||
@override
|
||
Widget build(BuildContext context) => MwPanel(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Text('Настройки', style: Theme.of(context).textTheme.headlineMedium),
|
||
const SizedBox(height: 20),
|
||
const Text('Папка установки'),
|
||
const SizedBox(height: 6),
|
||
MwStatusText(
|
||
text: installationPath ?? 'Папка не выбрана',
|
||
monospace: true,
|
||
),
|
||
const SizedBox(height: 8),
|
||
MwButton(
|
||
label: 'Выбрать папку',
|
||
icon: Icons.folder_open_outlined,
|
||
onPressed: syncActive ? null : onChooseDirectory,
|
||
variant: MwButtonVariant.secondary,
|
||
),
|
||
const SizedBox(height: 20),
|
||
SegmentedButton<MoonWellThemeVariant>(
|
||
segments: const [
|
||
ButtonSegment(
|
||
value: MoonWellThemeVariant.forest,
|
||
label: Text('Лес'),
|
||
),
|
||
ButtonSegment(
|
||
value: MoonWellThemeVariant.temple,
|
||
label: Text('Храм'),
|
||
),
|
||
],
|
||
selected: {themeVariant},
|
||
onSelectionChanged: (value) => onThemeChanged(value.single),
|
||
),
|
||
const SizedBox(height: 20),
|
||
MwButton(
|
||
label: 'Проверить файлы',
|
||
onPressed: syncActive ? null : onVerify,
|
||
variant: MwButtonVariant.secondary,
|
||
),
|
||
const SizedBox(height: 8),
|
||
MwButton(
|
||
label: 'Выйти',
|
||
onPressed: onLogout,
|
||
variant: MwButtonVariant.destructive,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|