Настройки, авторизация
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+117
-40
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user