Files
forkmessager/client-mobile/lib/features/settings/presentation/pages/settings_page.dart
T

560 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../domain/entities/server_config.dart';
import 'package:messenger_app/features/settings/presentation/bloc/settings_bloc.dart';
import 'package:messenger_app/features/settings/presentation/bloc/settings_event.dart';
import 'package:messenger_app/features/settings/presentation/bloc/settings_state.dart';
import 'package:messenger_app/features/auth/presentation/bloc/auth_bloc.dart';
import 'package:messenger_app/features/auth/presentation/bloc/auth_state.dart';
import 'package:messenger_app/features/auth/presentation/bloc/auth_event.dart';
import 'package:messenger_app/l10n/app_localizations.dart';
final sl = GetIt.instance;
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
// BlocProvider is now at the top level in app.dart
return const _SettingsContent();
}
}
class _SettingsContent extends StatelessWidget {
const _SettingsContent();
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings),
),
body: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
return state.when(
initial: () => _SettingsContentImpl(
apiUrl: '',
serverConfig: null,
languageCode: 'ru',
connectionStatus: ConnectionStatus.unknown,
),
loading: () => const Center(child: CircularProgressIndicator()),
loaded: (apiUrl, serverConfig, languageCode, error, connectionStatus) => _SettingsContentImpl(
apiUrl: apiUrl,
serverConfig: serverConfig,
languageCode: languageCode,
connectionStatus: connectionStatus,
connectionError: error,
),
error: (message) => Center(child: Text('${l10n.error}: $message')),
);
},
),
);
}
}
class _SettingsContentImpl extends StatelessWidget {
final String apiUrl;
final ServerConfig? serverConfig;
final String languageCode;
final ConnectionStatus connectionStatus;
final String? connectionError;
const _SettingsContentImpl({
required this.apiUrl,
required this.serverConfig,
required this.languageCode,
this.connectionStatus = ConnectionStatus.unknown,
this.connectionError,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListView(
children: [
_SectionHeader(title: l10n.serverSettings),
_ApiUrlTile(
apiUrl: apiUrl,
connectionStatus: connectionStatus,
connectionError: connectionError,
onSave: (url) {
context.read<SettingsBloc>().add(SettingsApiUrlSaved(url));
},
),
const Divider(),
_SectionHeader(title: l10n.language),
_LanguageTile(
currentCode: languageCode,
onChanged: (code) {
context.read<SettingsBloc>().add(SettingsLanguageChanged(code));
},
),
const Divider(),
if (serverConfig != null) ...[
_SectionHeader(title: l10n.serverConfig),
_ConfigModuleCard(
title: l10n.system,
icon: Icons.computer,
children: [
_ConfigItem(
label: l10n.domainUrl,
value: serverConfig!.system.domainUrl.contains('example.com') ? apiUrl : serverConfig!.system.domainUrl,
),
_ConfigItem(label: l10n.registrationStatus, value: serverConfig!.system.enableRegistration ? l10n.enabled : l10n.disabled),
],
),
_ConfigModuleCard(
title: l10n.messages,
icon: Icons.message,
children: [
_ConfigItem(
label: l10n.dailyLimit,
value: serverConfig!.messages.dailyMessageLimitPerUser == 0 ? l10n.noLimit : '${serverConfig!.messages.dailyMessageLimitPerUser}',
),
_ConfigItem(
label: l10n.historyLimit,
value: serverConfig!.messages.chatMessageLimit == 0 ? l10n.noLimit : '${serverConfig!.messages.chatMessageLimit}',
),
_ConfigItem(
label: l10n.maxFileSize,
value: '${(serverConfig!.messages.maxFileSize / 1024 / 1024).toStringAsFixed(0)} MB',
),
_ConfigItem(label: l10n.allowMedia, value: serverConfig!.messages.allowMedia ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.voice, value: serverConfig!.messages.allowVoiceMessages ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowForwarding, value: serverConfig!.messages.allowForwarding ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowReactions, value: serverConfig!.messages.allowReactions ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowReplies, value: serverConfig!.messages.allowReplies ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowQuoting, value: serverConfig!.messages.allowQuoting ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowMessageDeletion, value: serverConfig!.messages.allowMessageDeletion ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.forbidCopying, value: serverConfig!.messages.forbidCopying ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowLinks, value: serverConfig!.messages.allowLinks ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowPolls, value: serverConfig!.messages.allowPolls ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.allowPinning, value: serverConfig!.messages.allowPinning ? l10n.enabled : l10n.disabled),
],
),
_ConfigModuleCard(
title: l10n.calls,
icon: Icons.call,
children: [
_ConfigItem(label: l10n.enabled, value: serverConfig!.webRtc.enabled ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.voice, value: serverConfig!.webRtc.enableVoiceCalls ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.video, value: serverConfig!.webRtc.enableVideoCalls ? l10n.enabled : l10n.disabled),
],
),
_ConfigModuleCard(
title: l10n.stories,
icon: Icons.auto_stories,
children: [
_ConfigItem(label: l10n.enabled, value: serverConfig!.stories.enabled ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.lifetime, value: '${serverConfig!.stories.storyLifetimeHours}${l10n.hours}'),
],
),
_ConfigModuleCard(
title: l10n.chatsModule,
icon: Icons.chat_bubble,
children: [
_ConfigItem(label: l10n.groups, value: serverConfig!.chats.supportGroups ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.maxParticipants, value: '${serverConfig!.chats.maxGroupParticipants}'),
],
),
if (serverConfig!.federation.enabled)
_ConfigModuleCard(
title: l10n.federation,
icon: Icons.public,
children: [
_ConfigItem(label: l10n.description, value: serverConfig!.federation.serverDescription),
],
),
const Divider(),
],
if (serverConfig == null)
ListTile(
leading: const Icon(Icons.refresh),
title: Text(l10n.retry),
onTap: () {
context.read<SettingsBloc>().add(const SettingsRefreshConfig());
},
),
_SectionHeader(title: l10n.settings),
_SettingsTile(
icon: Icons.notifications_outlined,
title: l10n.notifications,
onTap: () {},
),
_SettingsTile(
icon: Icons.security,
title: l10n.privacy,
onTap: () {},
),
_SettingsTile(
icon: Icons.volume_up,
title: l10n.soundsAndVibration,
onTap: () {},
),
_SettingsTile(
icon: Icons.storage,
title: l10n.dataAndStorage,
onTap: () {},
),
const Divider(),
_SettingsTile(
icon: Icons.info_outline,
title: l10n.aboutApp,
subtitle: '${l10n.version} 1.0.0',
onTap: () {},
),
_SettingsTile(
icon: Icons.help_outline,
title: l10n.help,
onTap: () {},
),
const Divider(),
BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
final isAuthenticated = authState.when(
initial: () => false,
loading: () => false,
authenticated: (_) => true,
unauthenticated: () => false,
error: (_) => false,
);
if (!isAuthenticated) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.all(16),
child: OutlinedButton(
onPressed: () {
context.read<AuthBloc>().add(const AuthEvent.logoutRequested());
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(l10n.logout),
),
),
);
},
),
],
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title.toUpperCase(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
letterSpacing: 0.5,
),
),
);
}
}
class _ApiUrlTile extends StatefulWidget {
final String apiUrl;
final ConnectionStatus connectionStatus;
final String? connectionError;
final ValueChanged<String> onSave;
const _ApiUrlTile({
required this.apiUrl,
required this.connectionStatus,
this.connectionError,
required this.onSave,
});
@override
State<_ApiUrlTile> createState() => _ApiUrlTileState();
}
class _ApiUrlTileState extends State<_ApiUrlTile> {
late final TextEditingController _controller;
bool _isEditing = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.apiUrl);
}
@override
void didUpdateWidget(covariant _ApiUrlTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.apiUrl != widget.apiUrl) {
_controller.text = widget.apiUrl;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _buildStatusIndicator(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
switch (widget.connectionStatus) {
case ConnectionStatus.checking:
return const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
);
case ConnectionStatus.connected:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, color: Colors.green[600], size: 18),
const SizedBox(width: 4),
Text(
l10n.connected,
style: TextStyle(color: Colors.green[700], fontSize: 12),
),
],
);
case ConnectionStatus.error:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, color: Colors.red[600], size: 18),
const SizedBox(width: 4),
Flexible(
child: Text(
_getLocalError(context, widget.connectionError),
style: TextStyle(color: Colors.red[700], fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
);
case ConnectionStatus.unknown:
return const SizedBox.shrink();
}
}
String _getLocalError(BuildContext context, String? errorKey) {
final l10n = AppLocalizations.of(context)!;
if (errorKey == null) return l10n.serverConnectionError;
switch (errorKey) {
case 'unknownError': return l10n.unknownError;
case 'networkError': return l10n.networkError;
case 'serverError': return l10n.serverError;
case 'parsingError': return l10n.parsingError;
default: return errorKey;
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (_isEditing) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
labelText: l10n.apiUrl,
hintText: l10n.apiUrlHint,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
keyboardType: TextInputType.url,
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.check, color: Colors.green),
onPressed: () {
widget.onSave(_controller.text.trim());
setState(() => _isEditing = false);
},
),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () {
_controller.text = widget.apiUrl;
setState(() => _isEditing = false);
},
),
],
),
);
}
return ListTile(
leading: const Icon(Icons.link),
title: Text(l10n.apiUrl),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.apiUrl.isEmpty ? l10n.apiUrlHint : widget.apiUrl,
style: TextStyle(
color: widget.apiUrl.isEmpty ? Colors.grey : null,
),
),
const SizedBox(height: 2),
_buildStatusIndicator(context),
],
),
trailing: const Icon(Icons.edit),
onTap: () => setState(() => _isEditing = true),
);
}
}
class _LanguageTile extends StatelessWidget {
final String currentCode;
final ValueChanged<String> onChanged;
const _LanguageTile({
required this.currentCode,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.language),
subtitle: Text(currentCode == 'ru' ? l10n.russian : l10n.english),
trailing: DropdownButton<String>(
value: currentCode,
underline: const SizedBox(),
items: [
DropdownMenuItem(value: 'ru', child: Text(l10n.russian)),
DropdownMenuItem(value: 'en', child: Text(l10n.english)),
],
onChanged: (value) {
if (value != null) onChanged(value);
},
),
);
}
}
class _ConfigModuleCard extends StatelessWidget {
final String title;
final IconData icon;
final List<Widget> children;
const _ConfigModuleCard({
required this.title,
required this.icon,
required this.children,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ExpansionTile(
leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
title: Text(title),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Column(
children: children,
),
),
],
),
);
}
}
class _ConfigItem extends StatelessWidget {
final String label;
final String value;
const _ConfigItem({
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
),
),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
);
}
}
class _SettingsTile extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final VoidCallback onTap;
const _SettingsTile({
required this.icon,
required this.title,
this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}