Приложение на флаторе, настройки
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../../domain/entities/server_config.dart';
|
||||
import '../bloc/settings_bloc.dart';
|
||||
import '../bloc/settings_event.dart';
|
||||
import '../bloc/settings_state.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SettingsBloc, SettingsState>(
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
initial: () => const Center(child: CircularProgressIndicator()),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
loaded: (apiUrl, serverConfig, languageCode, error) => _SettingsContent(
|
||||
apiUrl: apiUrl,
|
||||
serverConfig: serverConfig,
|
||||
languageCode: languageCode,
|
||||
error: error,
|
||||
),
|
||||
error: (message) => Center(child: Text(message)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsContent extends StatelessWidget {
|
||||
final String apiUrl;
|
||||
final ServerConfig? serverConfig;
|
||||
final String languageCode;
|
||||
final String? error;
|
||||
|
||||
const _SettingsContent({
|
||||
required this.apiUrl,
|
||||
required this.serverConfig,
|
||||
required this.languageCode,
|
||||
this.error,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_SectionHeader(title: l10n.serverSettings),
|
||||
_ApiUrlTile(
|
||||
apiUrl: apiUrl,
|
||||
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),
|
||||
_ConfigItem(label: l10n.enableRegistration, value: serverConfig!.system.enableRegistration ? l10n.enabled : l10n.disabled),
|
||||
],
|
||||
),
|
||||
_ConfigModuleCard(
|
||||
title: l10n.messages,
|
||||
icon: Icons.message,
|
||||
children: [
|
||||
_ConfigItem(label: l10n.enabled, value: serverConfig!.messages.allowMedia ? l10n.enabled : l10n.disabled),
|
||||
_ConfigItem(label: 'Max media size', value: '${(serverConfig!.messages.maxMediaSizeBytes / 1024 / 1024).toStringAsFixed(1)} MB'),
|
||||
],
|
||||
),
|
||||
_ConfigModuleCard(
|
||||
title: l10n.calls,
|
||||
icon: Icons.call,
|
||||
children: [
|
||||
_ConfigItem(label: l10n.enabled, value: serverConfig!.webRtc.enabled ? l10n.enabled : l10n.disabled),
|
||||
_ConfigItem(label: 'Voice calls', value: serverConfig!.webRtc.enableVoiceCalls ? l10n.enabled : l10n.disabled),
|
||||
_ConfigItem(label: 'Video calls', 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: 'Lifetime', value: '${serverConfig!.stories.storyLifetimeHours}h'),
|
||||
],
|
||||
),
|
||||
_ConfigModuleCard(
|
||||
title: l10n.chatsModule,
|
||||
icon: Icons.chat_bubble,
|
||||
children: [
|
||||
_ConfigItem(label: 'Groups', value: serverConfig!.chats.supportGroups ? l10n.enabled : l10n.disabled),
|
||||
_ConfigItem(label: 'Max participants', value: '${serverConfig!.chats.maxGroupParticipants}'),
|
||||
],
|
||||
),
|
||||
if (serverConfig!.federation.enabled)
|
||||
_ConfigModuleCard(
|
||||
title: l10n.federation,
|
||||
icon: Icons.public,
|
||||
children: [
|
||||
_ConfigItem(label: '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(),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
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 ValueChanged<String> onSave;
|
||||
|
||||
const _ApiUrlTile({
|
||||
required this.apiUrl,
|
||||
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();
|
||||
}
|
||||
|
||||
@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: Text(
|
||||
widget.apiUrl.isEmpty ? l10n.apiUrlHint : widget.apiUrl,
|
||||
style: TextStyle(
|
||||
color: widget.apiUrl.isEmpty ? Colors.grey : null,
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user