Настройки, авторизация

This commit is contained in:
Халимов Рустам
2026-05-14 12:06:34 +03:00
parent e8161d23d4
commit ccffc5a53c
41 changed files with 2556 additions and 1375 deletions
@@ -1,4 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../../../core/errors/errors.dart';
import '../../domain/repositories/settings_repository.dart';
import 'settings_event.dart';
import 'settings_state.dart';
@@ -17,54 +20,64 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
Future<void> _onStarted(SettingsStarted event, emit) async {
emit(const SettingsState.loading());
final apiUrl = await _repository.getApiUrl() ?? '';
final languageCode = await _repository.getLanguageCode() ?? 'ru';
if (apiUrl.isEmpty) {
emit(SettingsState.loaded(
apiUrl: '',
serverConfig: null,
languageCode: languageCode,
));
return;
}
final configResult = await _repository.getServerConfig();
configResult.when(
onSuccess: (config) {
emit(SettingsState.loaded(
apiUrl: apiUrl,
serverConfig: config,
languageCode: languageCode,
));
},
onFailure: (error) {
emit(SettingsState.loaded(
apiUrl: apiUrl,
serverConfig: null,
languageCode: languageCode,
error: 'Failed to load server config',
));
},
);
if (configResult.isSuccess) {
emit(SettingsState.loaded(
apiUrl: apiUrl,
serverConfig: configResult.data,
languageCode: languageCode,
connectionStatus: ConnectionStatus.connected,
));
} else {
emit(SettingsState.loaded(
apiUrl: apiUrl,
serverConfig: null,
languageCode: languageCode,
connectionStatus: ConnectionStatus.error,
error: _errorMessage(configResult.error!),
));
}
}
Future<void> _onConfigRequested(SettingsConfigRequested event, emit) async {
if (state is! SettingsLoaded) return;
final currentState = state as SettingsLoaded;
emit(const SettingsState.loading());
final configResult = await _repository.getServerConfig();
configResult.when(
onSuccess: (config) {
emit(SettingsState.loaded(
apiUrl: currentState.apiUrl,
serverConfig: config,
languageCode: currentState.languageCode,
));
},
onFailure: (error) {
emit(SettingsState.loaded(
apiUrl: currentState.apiUrl,
serverConfig: null,
languageCode: currentState.languageCode,
error: 'Failed to refresh config',
));
},
);
if (currentState.apiUrl.isEmpty) return;
emit(currentState.copyWith(connectionStatus: ConnectionStatus.checking));
final configResult = await _repository.refreshServerConfig();
if (configResult.isSuccess) {
emit(SettingsState.loaded(
apiUrl: currentState.apiUrl,
serverConfig: configResult.data,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.connected,
));
} else {
emit(SettingsState.loaded(
apiUrl: currentState.apiUrl,
serverConfig: null,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.error,
error: _errorMessage(configResult.error!),
));
}
}
void _onApiUrlChanged(SettingsApiUrlChanged event, emit) {
@@ -74,6 +87,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
apiUrl: event.url,
serverConfig: currentState.serverConfig,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.unknown,
));
}
@@ -81,25 +95,64 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
if (state is! SettingsLoaded) return;
final currentState = state as SettingsLoaded;
await _repository.saveApiUrl(event.url);
// Нормализуем URL: убираем дублирование /api и другие проблемные суффиксы
final normalizedUrl = _normalizeApiUrl(event.url);
await _repository.saveApiUrl(normalizedUrl);
// Обновляем baseUrl у Dio, чтобы последующие запросы шли на новый адрес
final dio = GetIt.instance<Dio>();
dio.options.baseUrl = normalizedUrl;
if (event.url.isEmpty) {
emit(SettingsState.loaded(
apiUrl: '',
serverConfig: null,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.unknown,
));
return;
}
// Проверяем подключение к новому адресу
emit(SettingsState.loaded(
apiUrl: event.url,
serverConfig: currentState.serverConfig,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.checking,
));
final configResult = await _repository.refreshServerConfig();
if (configResult.isSuccess) {
emit(SettingsState.loaded(
apiUrl: event.url,
serverConfig: configResult.data,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.connected,
));
} else {
emit(SettingsState.loaded(
apiUrl: event.url,
serverConfig: null,
languageCode: currentState.languageCode,
connectionStatus: ConnectionStatus.error,
error: _errorMessage(configResult.error!),
));
}
}
Future<void> _onLanguageChanged(SettingsLanguageChanged event, emit) async {
if (state is! SettingsLoaded) return;
final currentState = state as SettingsLoaded;
await _repository.saveLanguageCode(event.code);
emit(SettingsState.loaded(
apiUrl: currentState.apiUrl,
serverConfig: currentState.serverConfig,
languageCode: event.code,
connectionStatus: currentState.connectionStatus,
));
}
@@ -107,4 +160,33 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
add(const SettingsConfigRequested());
await Future.delayed(const Duration(milliseconds: 100));
}
String _errorMessage(AppError error) => error.when(
unknown: (msg) => msg?.contains('parsing') == true ? 'parsingError' : 'unknownError',
network: (msg) => 'networkError',
unauthorized: (msg) => 'unauthorized',
forbidden: (msg) => 'forbidden',
notFound: (msg) => 'notFound',
server: (code, msg) => 'serverError',
database: (msg) => 'databaseError',
validation: (message, errors) => 'validationError',
parsing: (msg) => 'parsingError',
);
String _normalizeApiUrl(String url) {
if (url.isEmpty) return url;
// Убираем все слэши в конце
String normalized = url.replaceAll(RegExp(r'/+$'), '');
// Если URL заканчивается на /api, убираем этот суффикс
// (так как мы всегда добавляем /api к путям при запросе)
if (normalized.endsWith('/api')) {
normalized = normalized.substring(0, normalized.length - 4);
normalized = normalized.replaceAll(RegExp(r'/+$'), '');
}
return normalized;
}
}
@@ -3,6 +3,8 @@ import '../../domain/entities/server_config.dart';
part 'settings_state.freezed.dart';
enum ConnectionStatus { unknown, checking, connected, error }
@freezed
class SettingsState with _$SettingsState {
const factory SettingsState.initial() = SettingsInitial;
@@ -12,6 +14,7 @@ class SettingsState with _$SettingsState {
required ServerConfig? serverConfig,
required String languageCode,
String? error,
@Default(ConnectionStatus.unknown) ConnectionStatus connectionStatus,
}) = SettingsLoaded;
const factory SettingsState.error(String message) = SettingsError;
}
@@ -20,8 +20,12 @@ mixin _$SettingsState {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)
required TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)
loaded,
required TResult Function(String message) error,
}) =>
@@ -30,8 +34,12 @@ mixin _$SettingsState {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult? Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult? Function(String message)? error,
}) =>
@@ -40,8 +48,12 @@ mixin _$SettingsState {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
@@ -132,8 +144,12 @@ class _$SettingsInitialImpl implements SettingsInitial {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)
required TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)
loaded,
required TResult Function(String message) error,
}) {
@@ -145,8 +161,12 @@ class _$SettingsInitialImpl implements SettingsInitial {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult? Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult? Function(String message)? error,
}) {
@@ -158,8 +178,12 @@ class _$SettingsInitialImpl implements SettingsInitial {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
@@ -252,8 +276,12 @@ class _$SettingsLoadingImpl implements SettingsLoading {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)
required TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)
loaded,
required TResult Function(String message) error,
}) {
@@ -265,8 +293,12 @@ class _$SettingsLoadingImpl implements SettingsLoading {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult? Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult? Function(String message)? error,
}) {
@@ -278,8 +310,12 @@ class _$SettingsLoadingImpl implements SettingsLoading {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
@@ -342,7 +378,8 @@ abstract class _$$SettingsLoadedImplCopyWith<$Res> {
{String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error});
String? error,
ConnectionStatus connectionStatus});
$ServerConfigCopyWith<$Res>? get serverConfig;
}
@@ -362,6 +399,7 @@ class __$$SettingsLoadedImplCopyWithImpl<$Res>
Object? serverConfig = freezed,
Object? languageCode = null,
Object? error = freezed,
Object? connectionStatus = null,
}) {
return _then(_$SettingsLoadedImpl(
apiUrl: null == apiUrl
@@ -380,6 +418,10 @@ class __$$SettingsLoadedImplCopyWithImpl<$Res>
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
connectionStatus: null == connectionStatus
? _value.connectionStatus
: connectionStatus // ignore: cast_nullable_to_non_nullable
as ConnectionStatus,
));
}
@@ -403,7 +445,8 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
{required this.apiUrl,
required this.serverConfig,
required this.languageCode,
this.error});
this.error,
this.connectionStatus = ConnectionStatus.unknown});
@override
final String apiUrl;
@@ -413,10 +456,13 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
final String languageCode;
@override
final String? error;
@override
@JsonKey()
final ConnectionStatus connectionStatus;
@override
String toString() {
return 'SettingsState.loaded(apiUrl: $apiUrl, serverConfig: $serverConfig, languageCode: $languageCode, error: $error)';
return 'SettingsState.loaded(apiUrl: $apiUrl, serverConfig: $serverConfig, languageCode: $languageCode, error: $error, connectionStatus: $connectionStatus)';
}
@override
@@ -429,12 +475,14 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
other.serverConfig == serverConfig) &&
(identical(other.languageCode, languageCode) ||
other.languageCode == languageCode) &&
(identical(other.error, error) || other.error == error));
(identical(other.error, error) || other.error == error) &&
(identical(other.connectionStatus, connectionStatus) ||
other.connectionStatus == connectionStatus));
}
@override
int get hashCode =>
Object.hash(runtimeType, apiUrl, serverConfig, languageCode, error);
int get hashCode => Object.hash(
runtimeType, apiUrl, serverConfig, languageCode, error, connectionStatus);
@JsonKey(ignore: true)
@override
@@ -448,12 +496,17 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)
required TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)
loaded,
required TResult Function(String message) error,
}) {
return loaded(apiUrl, serverConfig, languageCode, this.error);
return loaded(
apiUrl, serverConfig, languageCode, this.error, connectionStatus);
}
@override
@@ -461,12 +514,17 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult? Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult? Function(String message)? error,
}) {
return loaded?.call(apiUrl, serverConfig, languageCode, this.error);
return loaded?.call(
apiUrl, serverConfig, languageCode, this.error, connectionStatus);
}
@override
@@ -474,14 +532,19 @@ class _$SettingsLoadedImpl implements SettingsLoaded {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(apiUrl, serverConfig, languageCode, this.error);
return loaded(
apiUrl, serverConfig, languageCode, this.error, connectionStatus);
}
return orElse();
}
@@ -529,12 +592,14 @@ abstract class SettingsLoaded implements SettingsState {
{required final String apiUrl,
required final ServerConfig? serverConfig,
required final String languageCode,
final String? error}) = _$SettingsLoadedImpl;
final String? error,
final ConnectionStatus connectionStatus}) = _$SettingsLoadedImpl;
String get apiUrl;
ServerConfig? get serverConfig;
String get languageCode;
String? get error;
ConnectionStatus get connectionStatus;
@JsonKey(ignore: true)
_$$SettingsLoadedImplCopyWith<_$SettingsLoadedImpl> get copyWith =>
throw _privateConstructorUsedError;
@@ -606,8 +671,12 @@ class _$SettingsErrorImpl implements SettingsError {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)
required TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)
loaded,
required TResult Function(String message) error,
}) {
@@ -619,8 +688,12 @@ class _$SettingsErrorImpl implements SettingsError {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult? Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult? Function(String message)? error,
}) {
@@ -632,8 +705,12 @@ class _$SettingsErrorImpl implements SettingsError {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(String apiUrl, ServerConfig? serverConfig,
String languageCode, String? error)?
TResult Function(
String apiUrl,
ServerConfig? serverConfig,
String languageCode,
String? error,
ConnectionStatus connectionStatus)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
@@ -1,56 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../domain/entities/server_config.dart';
import '../bloc/settings_bloc.dart';
import '../bloc/settings_event.dart';
import '../bloc/settings_state.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) {
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)),
);
},
);
// 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 String? error;
final ConnectionStatus connectionStatus;
final String? connectionError;
const _SettingsContent({
const _SettingsContentImpl({
required this.apiUrl,
required this.serverConfig,
required this.languageCode,
this.error,
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));
},
@@ -72,16 +104,40 @@ class _SettingsContent extends StatelessWidget {
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),
_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.enabled, value: serverConfig!.messages.allowMedia ? l10n.enabled : l10n.disabled),
_ConfigItem(label: 'Max media size', value: '${(serverConfig!.messages.maxMediaSizeBytes / 1024 / 1024).toStringAsFixed(1)} MB'),
_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(
@@ -89,8 +145,8 @@ class _SettingsContent extends StatelessWidget {
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),
_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(
@@ -98,15 +154,15 @@ class _SettingsContent extends StatelessWidget {
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'),
_ConfigItem(label: l10n.lifetime, value: '${serverConfig!.stories.storyLifetimeHours}${l10n.hours}'),
],
),
_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}'),
_ConfigItem(label: l10n.groups, value: serverConfig!.chats.supportGroups ? l10n.enabled : l10n.disabled),
_ConfigItem(label: l10n.maxParticipants, value: '${serverConfig!.chats.maxGroupParticipants}'),
],
),
if (serverConfig!.federation.enabled)
@@ -114,7 +170,7 @@ class _SettingsContent extends StatelessWidget {
title: l10n.federation,
icon: Icons.public,
children: [
_ConfigItem(label: 'Description', value: serverConfig!.federation.serverDescription),
_ConfigItem(label: l10n.description, value: serverConfig!.federation.serverDescription),
],
),
const Divider(),
@@ -165,19 +221,35 @@ class _SettingsContent extends StatelessWidget {
),
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),
),
),
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),
),
),
);
},
),
],
);
@@ -208,10 +280,14 @@ class _SectionHeader extends StatelessWidget {
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,
});
@@ -243,10 +319,63 @@ class _ApiUrlTileState extends State<_ApiUrlTile> {
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),
@@ -287,11 +416,18 @@ class _ApiUrlTileState extends State<_ApiUrlTile> {
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,
),
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),
@@ -311,7 +447,6 @@ class _LanguageTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.language),