Files
ignis_app/lib/screens/settings_screen.dart
2026-05-16 17:24:28 +07:00

906 lines
32 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/build_info.dart';
import '../app/error_message.dart';
import '../app/load_state.dart';
import '../features/settings/models/app_theme_preset.dart';
import '../features/settings/models/geofence_system_state.dart';
import '../features/settings/models/notification_permission_status.dart';
import '../features/settings/providers/settings_providers.dart';
import '../models/home_config.dart';
import '../providers/providers.dart';
import 'home_edit_screen.dart';
import 'homes_screen.dart';
import 'wiz_provisioning_screen.dart';
enum SettingsEntryPoint { homes, remote }
class SettingsScreen extends ConsumerStatefulWidget {
final SettingsEntryPoint entryPoint;
const SettingsScreen({super.key, required this.entryPoint});
@override
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen>
with WidgetsBindingObserver {
bool _savingGeofence = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.invalidate(geofenceSystemStatusProvider);
ref.invalidate(notificationPermissionStatusProvider);
}
}
@override
Widget build(BuildContext context) {
final currentHome = ref.watch(currentHomeProvider);
final authState = ref.watch(authInfoProvider);
final themePreset = ref.watch(appThemeProvider);
final geofenceStatus = ref.watch(geofenceSystemStatusProvider);
final geofenceState = geofenceStatus.asData?.value;
final notificationStatus = ref.watch(notificationPermissionStatusProvider);
final notificationPermission = notificationStatus.asData?.value;
final bottomInset = MediaQuery.paddingOf(context).bottom;
return Scaffold(
appBar: AppBar(title: const Text('НАСТРОЙКИ')),
body: ListView(
padding: EdgeInsets.fromLTRB(16, 16, 16, bottomInset + 24),
children: [
_SectionTitle(
title: 'Дом и подключение',
subtitle: 'К какому backend сейчас привязано приложение',
),
_SectionCard(
children: [
if (currentHome == null)
const _EmptySectionState(
icon: Icons.home_outlined,
title: 'Активный дом не выбран',
message: 'Выберите или добавьте дом, чтобы управлять светом.',
)
else ...[
_InfoRow(label: 'Дом', value: currentHome.name),
_InfoRow(label: 'Backend', value: currentHome.url),
_InfoRow(
label: 'Доступ',
value: _authSummary(authState),
muted: authState.status == LoadStatus.idle,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const WizProvisioningScreen(),
),
),
icon: const Icon(Icons.lightbulb_outline),
label: const Text('Подключить WiZ-лампу'),
),
FilledButton.tonalIcon(
onPressed: () => _openHomeEditor(context, currentHome),
icon: const Icon(Icons.edit_location_alt_outlined),
label: const Text('Редактировать дом'),
),
OutlinedButton.icon(
onPressed: () => _openHomes(context),
icon: const Icon(Icons.home_work_outlined),
label: const Text('Дома'),
),
],
),
],
],
),
const SizedBox(height: 20),
_SectionTitle(
title: 'Гео-автоматизация',
subtitle: 'Автовыключение света при уходе из дома',
),
_SectionCard(
children: [
if (currentHome == null)
const _EmptySectionState(
icon: Icons.location_off_outlined,
title: 'Сначала нужен активный дом',
message: 'Без выбранного дома geofence настраивать нечего.',
)
else ...[
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: Icon(
Icons.directions_walk,
color: currentHome.hasCoordinates
? Theme.of(context).colorScheme.primary
: Theme.of(context).disabledColor,
),
title: const Text('Выключать свет при уходе'),
subtitle: Text(
currentHome.hasCoordinates
? 'Работает только для текущего активного дома'
: 'Сначала задайте координаты дома',
),
value: currentHome.geofenceEnabled,
onChanged: currentHome.hasCoordinates && !_savingGeofence
? (enabled) => _setGeofenceEnabled(currentHome, enabled)
: null,
),
const Divider(height: 24),
_StatusTile(
icon: _statusIcon(geofenceState, currentHome),
color: _statusColor(context, geofenceState, currentHome),
title: _statusTitle(geofenceState, currentHome),
subtitle: _statusSubtitle(geofenceState, currentHome),
),
const SizedBox(height: 12),
_PermissionTile(
icon: _locationPermissionIcon(geofenceState),
color: _locationPermissionColor(context, geofenceState),
title: 'Геолокация',
subtitle: _locationPermissionSubtitle(geofenceState),
),
const SizedBox(height: 8),
_PermissionTile(
icon: _notificationPermissionIcon(notificationPermission),
color: _notificationPermissionColor(
context,
notificationPermission,
),
title: 'Уведомления',
subtitle: _notificationPermissionSubtitle(
notificationPermission,
),
),
if (_savingGeofence)
const Padding(
padding: EdgeInsets.only(top: 12),
child: LinearProgressIndicator(minHeight: 3),
),
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.radar_outlined),
title: const Text('Радиус срабатывания'),
subtitle: Text('${currentHome.geofenceRadiusMeters} м'),
trailing: const Icon(Icons.chevron_right),
enabled: currentHome.hasCoordinates && !_savingGeofence,
onTap: currentHome.hasCoordinates && !_savingGeofence
? () => _editGeofenceRadius(context, currentHome)
: null,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _buildGeofenceActions(
context: context,
home: currentHome,
systemState: geofenceState,
notificationPermission: notificationPermission,
),
),
],
],
),
const SizedBox(height: 20),
_SectionTitle(
title: 'Внешний вид',
subtitle: 'Фиксированные темы без лишней кастомизации',
),
_SectionCard(
children: [
RadioGroup<AppThemePreset>(
groupValue: themePreset,
onChanged: (value) {
if (value != null) {
ref.read(appThemeProvider.notifier).setTheme(value);
}
},
child: Column(
children: [
for (final preset in AppThemePreset.values)
RadioListTile<AppThemePreset>(
value: preset,
activeColor: preset.accentColor,
secondary: CircleAvatar(
radius: 12,
backgroundColor: preset.accentColor,
),
title: Text(preset.title),
subtitle: Text(preset.subtitle),
),
],
),
),
],
),
const SizedBox(height: 20),
_SectionTitle(
title: 'О приложении',
subtitle: 'Короткая техническая сводка без маркетинговой воды',
),
_SectionCard(
children: [
const _InfoRow(label: 'Приложение', value: 'Ignis App'),
_InfoRow(label: 'Сборка', value: BuildInfo.label),
_InfoRow(label: 'Тема', value: themePreset.title),
_InfoRow(
label: 'Backend',
value: currentHome?.url ?? 'не выбран',
muted: currentHome == null,
),
const SizedBox(height: 12),
FilledButton.tonalIcon(
onPressed: () => _copyDiagnosticsSummary(
currentHome,
authState,
themePreset,
),
icon: const Icon(Icons.content_copy_outlined),
label: const Text('Скопировать сводку'),
),
],
),
],
),
);
}
List<Widget> _buildGeofenceActions({
required BuildContext context,
required HomeConfig home,
required GeofenceSystemState? systemState,
required NotificationPermissionStatus? notificationPermission,
}) {
final locationNotifier = ref.read(userLocationProvider.notifier);
final notificationNotifier = ref.read(
notificationPermissionStatusServiceProvider,
);
final issue = systemState?.issue;
if (!home.hasCoordinates) {
return [
FilledButton.tonalIcon(
onPressed: () => _openHomeEditor(context, home),
icon: const Icon(Icons.place_outlined),
label: const Text('Задать координаты'),
),
];
}
final actions = switch (issue) {
GeofenceSystemIssue.locationServicesDisabled => [
FilledButton.tonalIcon(
onPressed: () async {
await locationNotifier.openLocationSettings();
ref.invalidate(geofenceSystemStatusProvider);
},
icon: const Icon(Icons.location_searching_outlined),
label: const Text('Включить геолокацию'),
),
],
GeofenceSystemIssue.permissionDenied => [
FilledButton.tonalIcon(
onPressed: () async {
await locationNotifier.requestPermission();
ref.invalidate(geofenceSystemStatusProvider);
},
icon: const Icon(Icons.verified_user_outlined),
label: const Text('Запросить доступ'),
),
],
GeofenceSystemIssue.permissionDeniedForever ||
GeofenceSystemIssue.backgroundPermissionRequired => [
FilledButton.tonalIcon(
onPressed: () async {
await locationNotifier.openAppSettings();
ref.invalidate(geofenceSystemStatusProvider);
},
icon: const Icon(Icons.settings_outlined),
label: const Text('Открыть настройки Android'),
),
],
_ => [
OutlinedButton.icon(
onPressed: () => _openHomeEditor(context, home),
icon: const Icon(Icons.edit_outlined),
label: const Text('Координаты дома'),
),
],
};
if (notificationPermission == NotificationPermissionStatus.requestable) {
actions.add(
FilledButton.tonalIcon(
onPressed: () async {
await notificationNotifier.requestPermission();
ref.invalidate(notificationPermissionStatusProvider);
},
icon: const Icon(Icons.notifications_active_outlined),
label: const Text('Разрешить уведомления'),
),
);
}
if (notificationPermission ==
NotificationPermissionStatus.settingsRequired) {
actions.add(
OutlinedButton.icon(
onPressed: () async {
await notificationNotifier.openSettings();
ref.invalidate(notificationPermissionStatusProvider);
},
icon: const Icon(Icons.notifications_outlined),
label: const Text('Уведомления Android'),
),
);
}
return actions;
}
Future<void> _setGeofenceEnabled(HomeConfig home, bool enabled) async {
await _saveCurrentHome(
home.copyWith(geofenceEnabled: enabled),
successMessage: enabled
? 'Автовыключение включено'
: 'Автовыключение выключено',
);
}
Future<void> _saveCurrentHome(
HomeConfig updatedHome, {
String? successMessage,
}) async {
setState(() => _savingGeofence = true);
try {
await ref.read(homesProvider.notifier).update(updatedHome);
await ref.read(currentHomeProvider.notifier).switchTo(updatedHome);
ref.invalidate(geofenceSystemStatusProvider);
if (mounted && successMessage != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(successMessage)));
}
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Не удалось сохранить настройки: ${describeLoadError(error)}',
),
),
);
}
} finally {
if (mounted) {
setState(() => _savingGeofence = false);
}
}
}
Future<void> _editGeofenceRadius(
BuildContext context,
HomeConfig home,
) async {
var draft = home.geofenceRadiusMeters.toDouble();
final saved = await showModalBottomSheet<int>(
context: context,
showDragHandle: true,
builder: (sheetContext) {
return StatefulBuilder(
builder: (context, setModalState) {
final roundedDraft = (draft / 50).round() * 50;
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Радиус geofence',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'$roundedDraft м',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Slider(
value: draft.clamp(100, 5000),
min: 100,
max: 5000,
divisions: 98,
label: '$roundedDraft м',
onChanged: (value) {
setModalState(() => draft = value);
},
),
const SizedBox(height: 4),
const Text(
'Чем больше радиус, тем меньше шанс ложного срабатывания далеко от дома.',
),
const SizedBox(height: 16),
Row(
children: [
TextButton(
onPressed: () => Navigator.of(sheetContext).pop(),
child: const Text('Отмена'),
),
const Spacer(),
FilledButton(
onPressed: () =>
Navigator.of(sheetContext).pop(roundedDraft),
child: const Text('Сохранить'),
),
],
),
],
),
),
);
},
);
},
);
if (saved == null || saved == home.geofenceRadiusMeters) {
return;
}
await _saveCurrentHome(
home.copyWith(geofenceRadiusMeters: saved),
successMessage: 'Радиус geofence обновлён',
);
}
Future<void> _copyDiagnosticsSummary(
HomeConfig? currentHome,
LoadState authState,
AppThemePreset themePreset,
) async {
final lines = [
'app=Ignis App',
'build=${BuildInfo.label}',
'theme=${themePreset.storageValue}',
'home=${currentHome?.name ?? 'none'}',
'backend=${currentHome?.url ?? 'none'}',
'auth=${_authSummary(authState)}',
'geofence_enabled=${currentHome?.geofenceEnabled ?? false}',
'geofence_radius=${currentHome?.geofenceRadiusMeters ?? 0}',
'has_coordinates=${currentHome?.hasCoordinates == true}',
];
await Clipboard.setData(ClipboardData(text: lines.join('\n')));
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Сводка скопирована в буфер обмена')),
);
}
String _authSummary(LoadState authState) {
final auth = authState.data;
if (auth == null) {
if (authState.hasError) {
return authState.errorMessage ?? 'ошибка проверки доступа';
}
return 'не загружено';
}
final role = auth.isAdmin ? 'admin' : 'guest';
if (auth.name == null || auth.name!.trim().isEmpty) {
return role;
}
return '${auth.name} · $role';
}
IconData _statusIcon(GeofenceSystemState? state, HomeConfig home) {
if (!home.geofenceEnabled) {
return Icons.pause_circle_outline;
}
return switch (state?.issue) {
GeofenceSystemIssue.ready => Icons.verified_outlined,
GeofenceSystemIssue.locationServicesDisabled => Icons.location_disabled,
GeofenceSystemIssue.permissionDenied ||
GeofenceSystemIssue.permissionDeniedForever ||
GeofenceSystemIssue.backgroundPermissionRequired =>
Icons.gpp_bad_outlined,
GeofenceSystemIssue.missingCoordinates => Icons.place_outlined,
_ => Icons.info_outline,
};
}
Color _statusColor(
BuildContext context,
GeofenceSystemState? state,
HomeConfig home,
) {
if (!home.geofenceEnabled) {
return Theme.of(context).colorScheme.secondary;
}
return switch (state?.issue) {
GeofenceSystemIssue.ready => Colors.green,
GeofenceSystemIssue.locationServicesDisabled ||
GeofenceSystemIssue.permissionDenied ||
GeofenceSystemIssue.permissionDeniedForever ||
GeofenceSystemIssue.backgroundPermissionRequired ||
GeofenceSystemIssue.missingCoordinates => Theme.of(
context,
).colorScheme.error,
_ => Theme.of(context).colorScheme.primary,
};
}
String _statusTitle(GeofenceSystemState? state, HomeConfig home) {
if (!home.geofenceEnabled) {
return 'Автовыключение выключено';
}
return switch (state?.issue) {
GeofenceSystemIssue.ready => 'Geofence готов',
GeofenceSystemIssue.missingCoordinates => 'Нужны координаты дома',
GeofenceSystemIssue.locationServicesDisabled => 'Геолокация выключена',
GeofenceSystemIssue.permissionDenied => 'Нужен доступ к геолокации',
GeofenceSystemIssue.permissionDeniedForever => 'Доступ запрещён навсегда',
GeofenceSystemIssue.backgroundPermissionRequired =>
'Нужен доступ «Всегда»',
GeofenceSystemIssue.noActiveHome => 'Нет активного дома',
null => 'Проверяем системные условия',
};
}
String _statusSubtitle(GeofenceSystemState? state, HomeConfig home) {
if (!home.geofenceEnabled) {
return home.hasCoordinates
? 'Можно заранее настроить радиус и включить позже.'
: 'Сначала задайте координаты дома, потом включайте geofence.';
}
return switch (state?.issue) {
GeofenceSystemIssue.ready =>
'Android сможет отслеживать выход из зоны и запускать автовыключение.',
GeofenceSystemIssue.missingCoordinates =>
'Без координат дома geofence физически некуда поставить.',
GeofenceSystemIssue.locationServicesDisabled =>
'Пока системная геолокация выключена, фоновое срабатывание не поднимется.',
GeofenceSystemIssue.permissionDenied =>
'Нужно хотя бы базовое разрешение на геолокацию.',
GeofenceSystemIssue.permissionDeniedForever =>
'Разрешение нужно вернуть вручную в настройках Android.',
GeofenceSystemIssue.backgroundPermissionRequired =>
'Для фонового geofence нужен доступ к геолокации в режиме «Всегда».',
GeofenceSystemIssue.noActiveHome =>
'Сначала выберите активный дом, для которого будет работать автоматика.',
null => 'Собираем информацию о системных ограничениях.',
};
}
IconData _locationPermissionIcon(GeofenceSystemState? state) {
return switch (state?.issue) {
GeofenceSystemIssue.ready => Icons.verified_outlined,
GeofenceSystemIssue.locationServicesDisabled => Icons.location_disabled,
GeofenceSystemIssue.permissionDenied ||
GeofenceSystemIssue.permissionDeniedForever ||
GeofenceSystemIssue.backgroundPermissionRequired =>
Icons.gpp_bad_outlined,
_ => Icons.my_location_outlined,
};
}
Color _locationPermissionColor(
BuildContext context,
GeofenceSystemState? state,
) {
return switch (state?.issue) {
GeofenceSystemIssue.ready => Colors.green,
GeofenceSystemIssue.locationServicesDisabled ||
GeofenceSystemIssue.permissionDenied ||
GeofenceSystemIssue.permissionDeniedForever ||
GeofenceSystemIssue.backgroundPermissionRequired => Theme.of(
context,
).colorScheme.error,
_ => Colors.white70,
};
}
String _locationPermissionSubtitle(GeofenceSystemState? state) {
return switch (state?.issue) {
GeofenceSystemIssue.ready =>
'Доступ «Всегда», geofence может работать в фоне.',
GeofenceSystemIssue.locationServicesDisabled =>
'Системная геолокация сейчас выключена.',
GeofenceSystemIssue.permissionDenied =>
'Разрешение на геолокацию ещё не выдано.',
GeofenceSystemIssue.permissionDeniedForever =>
'Доступ к геолокации запрещён в Android.',
GeofenceSystemIssue.backgroundPermissionRequired =>
'Есть только доступ при использовании приложения.',
GeofenceSystemIssue.missingCoordinates =>
'Сначала задай координаты дома, потом появится полный статус.',
GeofenceSystemIssue.noActiveHome =>
'Без активного дома проверять нечего.',
null => 'Проверяем системный статус геолокации.',
};
}
IconData _notificationPermissionIcon(NotificationPermissionStatus? status) {
return switch (status) {
NotificationPermissionStatus.enabled =>
Icons.notifications_active_outlined,
NotificationPermissionStatus.requestable =>
Icons.notifications_off_outlined,
NotificationPermissionStatus.settingsRequired =>
Icons.notifications_paused_outlined,
NotificationPermissionStatus.unsupported ||
null => Icons.notifications_none_outlined,
};
}
Color _notificationPermissionColor(
BuildContext context,
NotificationPermissionStatus? status,
) {
return switch (status) {
NotificationPermissionStatus.enabled => Colors.green,
NotificationPermissionStatus.requestable ||
NotificationPermissionStatus.settingsRequired => Theme.of(
context,
).colorScheme.error,
NotificationPermissionStatus.unsupported || null => Colors.white70,
};
}
String _notificationPermissionSubtitle(NotificationPermissionStatus? status) {
return switch (status) {
NotificationPermissionStatus.enabled =>
'После срабатывания geofence приложение сможет прислать подтверждение.',
NotificationPermissionStatus.requestable =>
'Android ещё может показать системный запрос на уведомления.',
NotificationPermissionStatus.settingsRequired =>
'Автовыключение сработает и без них, но подтверждение нужно включить вручную.',
NotificationPermissionStatus.unsupported =>
'На этой платформе отдельный статус уведомлений не используется.',
null => 'Проверяем, доступны ли уведомления для подтверждения.',
};
}
Future<void> _openHomeEditor(BuildContext context, HomeConfig home) async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => HomeEditScreen(home: home)));
await ref.read(homesProvider.notifier).load();
await ref.read(currentHomeProvider.notifier).load();
ref.invalidate(geofenceSystemStatusProvider);
}
Future<void> _openHomes(BuildContext context) async {
if (widget.entryPoint == SettingsEntryPoint.homes) {
Navigator.of(context).pop();
return;
}
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (_) => const HomesScreen()));
}
}
class _SectionTitle extends StatelessWidget {
final String title;
final String subtitle;
const _SectionTitle({required this.title, required this.subtitle});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 2),
Text(
subtitle,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Colors.white54),
),
],
),
);
}
}
class _SectionCard extends StatelessWidget {
final List<Widget> children;
const _SectionCard({required this.children});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
);
}
}
class _InfoRow extends StatelessWidget {
final String label;
final String value;
final bool muted;
const _InfoRow({
required this.label,
required this.value,
this.muted = false,
});
@override
Widget build(BuildContext context) {
final valueStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(
color: muted ? Colors.white54 : null,
fontWeight: FontWeight.w600,
);
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Colors.white54),
),
const SizedBox(height: 2),
Text(value, style: valueStyle),
],
),
);
}
}
class _StatusTile extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
const _StatusTile({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.w700)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(color: Colors.white54)),
],
),
),
],
);
}
}
class _PermissionTile extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
const _PermissionTile({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(color: Colors.white54)),
],
),
),
],
);
}
}
class _EmptySectionState extends StatelessWidget {
final IconData icon;
final String title;
final String message;
const _EmptySectionState({
required this.icon,
required this.title,
required this.message,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: Colors.white38),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.w700)),
const SizedBox(height: 2),
Text(message, style: const TextStyle(color: Colors.white54)),
],
),
),
],
);
}
}