feat(auth): server endpoint in bottom sheet on login

This commit is contained in:
klockky
2026-04-07 13:36:30 +03:00
parent dc2a23ccf0
commit 91bdfea222
10 changed files with 408 additions and 14 deletions
+2 -1
View File
@@ -70,7 +70,8 @@ class Api {
}); });
try { try {
await _connection.connect(ServerConfig.host, ServerConfig.port); final endpoint = await ServerConfig.loadEndpoint();
await _connection.connect(endpoint.host, endpoint.port);
} catch (e) { } catch (e) {
logger.e('Не удалось подключиться: $e'); logger.e('Не удалось подключиться: $e');
_cleanup(); _cleanup();
+20 -2
View File
@@ -1,7 +1,25 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class ServerConfig { abstract class ServerConfig {
static const String host = 'api.oneme.ru'; static const String defaultHost = 'api.oneme.ru';
static const int port = 443; static const int defaultPort = 443;
static const String prefHostKey = 'server_host_override';
static const String prefPortKey = 'server_port_override';
static const Duration pingInterval = Duration(seconds: 30); static const Duration pingInterval = Duration(seconds: 30);
static const Duration requestTimeout = Duration(seconds: 30); static const Duration requestTimeout = Duration(seconds: 30);
static const int maxReconnectAttempts = 50; static const int maxReconnectAttempts = 50;
static Future<({String host, int port})> loadEndpoint() async {
final prefs = await SharedPreferences.getInstance();
final rawHost = prefs.getString(prefHostKey);
final rawPort = prefs.getInt(prefPortKey);
final host = (rawHost != null && rawHost.trim().isNotEmpty)
? rawHost.trim()
: defaultHost;
var port = defaultPort;
if (rawPort != null && rawPort >= 1 && rawPort <= 65535) {
port = rawPort;
}
return (host: host, port: port);
}
} }
+36 -3
View File
@@ -10,6 +10,7 @@ import 'package:komet/l10n/terms_of_service.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'code_confirmation_screen.dart'; import 'code_confirmation_screen.dart';
import 'select_country_screen.dart'; import 'select_country_screen.dart';
import 'server_settings_sheet.dart';
import 'spoff_redacted_screen.dart'; import 'spoff_redacted_screen.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../../main.dart'; import '../../../main.dart';
@@ -448,6 +449,23 @@ class _LoginScreenState extends State<LoginScreen> {
_showPhoneConfirmationDialog(_phoneController.text); _showPhoneConfirmationDialog(_phoneController.text);
} }
void _showServerSettingsSheet(BuildContext context) {
final cs = Theme.of(context).colorScheme;
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (_) {
return SafeArea(
child: const ServerSettingsSheet(),
);
},
);
}
void _showSecurityOptions(BuildContext context) { void _showSecurityOptions(BuildContext context) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
@@ -457,7 +475,7 @@ class _LoginScreenState extends State<LoginScreen> {
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
builder: (context) { builder: (sheetContext) {
return SafeArea( return SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -478,7 +496,7 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -498,7 +516,22 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
},
),
ListTile(
leading: Icon(Symbols.dns, color: cs.onSurface),
title: Text(
l10n.loginChangeServer,
style: GoogleFonts.inter(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
onTap: () {
Navigator.pop(sheetContext);
_showServerSettingsSheet(context);
}, },
), ),
], ],
@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:komet/backend/api.dart';
import 'package:komet/core/config/config.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../main.dart';
import '../../widgets/custom_notification.dart';
class ServerSettingsSheet extends StatefulWidget {
const ServerSettingsSheet({super.key});
@override
State<ServerSettingsSheet> createState() => _ServerSettingsSheetState();
}
class _ServerSettingsSheetState extends State<ServerSettingsSheet> {
final TextEditingController _hostController = TextEditingController(
text: ServerConfig.defaultHost,
);
final TextEditingController _portController = TextEditingController(
text: '${ServerConfig.defaultPort}',
);
bool _busy = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final endpoint = await ServerConfig.loadEndpoint();
if (!mounted) return;
setState(() {
_hostController.text = endpoint.host;
_portController.text = '${endpoint.port}';
});
}
Future<void> _apply(AppLocalizations l10n) async {
final host = _hostController.text.trim();
final port = int.tryParse(_portController.text.trim());
if (host.isEmpty || port == null || port < 1 || port > 65535) {
showCustomNotification(context, l10n.serverInvalidHostOrPort);
return;
}
setState(() => _busy = true);
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(ServerConfig.prefHostKey, host);
await prefs.setInt(ServerConfig.prefPortKey, port);
await api.disconnect();
await api.connect();
if (!mounted) return;
if (api.state == SessionState.online) {
showCustomNotification(context, l10n.serverSettingsSaved);
} else {
showCustomNotification(context, l10n.serverReconnectFailed);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _resetToDefault(AppLocalizations l10n) async {
setState(() => _busy = true);
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(ServerConfig.prefHostKey);
await prefs.remove(ServerConfig.prefPortKey);
_hostController.text = ServerConfig.defaultHost;
_portController.text = '${ServerConfig.defaultPort}';
await api.disconnect();
await api.connect();
if (!mounted) return;
if (api.state == SessionState.online) {
showCustomNotification(context, l10n.serverSettingsSaved);
} else {
showCustomNotification(context, l10n.serverReconnectFailed);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
void dispose() {
_hostController.dispose();
_portController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
return Padding(
padding: EdgeInsets.only(bottom: bottomInset),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: cs.onSurfaceVariant.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(2),
),
),
),
Text(
l10n.serverSettingsTitle,
style: GoogleFonts.inter(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 20),
_buildTextField(
controller: _hostController,
label: l10n.serverHostLabel,
hintText: ServerConfig.defaultHost,
cs: cs,
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
_buildTextField(
controller: _portController,
label: l10n.serverPortLabel,
hintText: '${ServerConfig.defaultPort}',
cs: cs,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
const SizedBox(height: 24),
FilledButton(
onPressed: _busy ? null : () => _apply(l10n),
child: Text(l10n.serverApply),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: _busy ? null : () => _resetToDefault(l10n),
child: Text(l10n.serverUseDefault),
),
],
),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required ColorScheme cs,
String? hintText,
TextInputType? keyboardType,
List<TextInputFormatter>? inputFormatters,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.inter(
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 8),
TextField(
controller: controller,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
enabled: !_busy,
style: GoogleFonts.inter(
color: cs.onSurface,
fontSize: 15,
),
decoration: InputDecoration(
hintText: hintText,
hintStyle: GoogleFonts.inter(
color: cs.onSurfaceVariant.withValues(alpha: 0.6),
fontSize: 15,
),
filled: true,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
],
);
}
}
+9
View File
@@ -15,6 +15,15 @@
"loginReadTermsNotification": "Please read the terms of use first", "loginReadTermsNotification": "Please read the terms of use first",
"loginSpoofRedacted": "Spoof redaction", "loginSpoofRedacted": "Spoof redaction",
"loginProxy": "Proxy", "loginProxy": "Proxy",
"loginChangeServer": "Change server",
"serverSettingsTitle": "Server",
"serverHostLabel": "Host",
"serverPortLabel": "Port",
"serverApply": "Apply and reconnect",
"serverUseDefault": "Reset to default",
"serverInvalidHostOrPort": "Enter a valid host and port (165535)",
"serverSettingsSaved": "Server settings applied",
"serverReconnectFailed": "Could not connect to the server",
"loginSignInWithQr": "Sign in with QR code", "loginSignInWithQr": "Sign in with QR code",
"loginSignInWithToken": "Sign in with token", "loginSignInWithToken": "Sign in with token",
"loginSignInWithSessionFile": "Sign in with session file", "loginSignInWithSessionFile": "Sign in with session file",
+54
View File
@@ -188,6 +188,60 @@ abstract class AppLocalizations {
/// **'Proxy'** /// **'Proxy'**
String get loginProxy; String get loginProxy;
/// No description provided for @loginChangeServer.
///
/// In en, this message translates to:
/// **'Change server'**
String get loginChangeServer;
/// No description provided for @serverSettingsTitle.
///
/// In en, this message translates to:
/// **'Server'**
String get serverSettingsTitle;
/// No description provided for @serverHostLabel.
///
/// In en, this message translates to:
/// **'Host'**
String get serverHostLabel;
/// No description provided for @serverPortLabel.
///
/// In en, this message translates to:
/// **'Port'**
String get serverPortLabel;
/// No description provided for @serverApply.
///
/// In en, this message translates to:
/// **'Apply and reconnect'**
String get serverApply;
/// No description provided for @serverUseDefault.
///
/// In en, this message translates to:
/// **'Reset to default'**
String get serverUseDefault;
/// No description provided for @serverInvalidHostOrPort.
///
/// In en, this message translates to:
/// **'Enter a valid host and port (165535)'**
String get serverInvalidHostOrPort;
/// No description provided for @serverSettingsSaved.
///
/// In en, this message translates to:
/// **'Server settings applied'**
String get serverSettingsSaved;
/// No description provided for @serverReconnectFailed.
///
/// In en, this message translates to:
/// **'Could not connect to the server'**
String get serverReconnectFailed;
/// No description provided for @loginSignInWithQr. /// No description provided for @loginSignInWithQr.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+27
View File
@@ -54,6 +54,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get loginProxy => 'Proxy'; String get loginProxy => 'Proxy';
@override
String get loginChangeServer => 'Change server';
@override
String get serverSettingsTitle => 'Server';
@override
String get serverHostLabel => 'Host';
@override
String get serverPortLabel => 'Port';
@override
String get serverApply => 'Apply and reconnect';
@override
String get serverUseDefault => 'Reset to default';
@override
String get serverInvalidHostOrPort => 'Enter a valid host and port (165535)';
@override
String get serverSettingsSaved => 'Server settings applied';
@override
String get serverReconnectFailed => 'Could not connect to the server';
@override @override
String get loginSignInWithQr => 'Sign in with QR code'; String get loginSignInWithQr => 'Sign in with QR code';
+28
View File
@@ -55,6 +55,34 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get loginProxy => 'Прокси'; String get loginProxy => 'Прокси';
@override
String get loginChangeServer => 'Смена сервера';
@override
String get serverSettingsTitle => 'Сервер';
@override
String get serverHostLabel => 'Хост';
@override
String get serverPortLabel => 'Порт';
@override
String get serverApply => 'Применить и переподключиться';
@override
String get serverUseDefault => 'Сбросить к умолчанию';
@override
String get serverInvalidHostOrPort =>
'Укажите корректный хост и порт (1–65535)';
@override
String get serverSettingsSaved => 'Настройки сервера применены';
@override
String get serverReconnectFailed => 'Не удалось подключиться к серверу';
@override @override
String get loginSignInWithQr => 'По QR code'; String get loginSignInWithQr => 'По QR code';
+9
View File
@@ -15,6 +15,15 @@
"loginReadTermsNotification": "Сначала прочитайте условия использования", "loginReadTermsNotification": "Сначала прочитайте условия использования",
"loginSpoofRedacted": "Подделка спуфа", "loginSpoofRedacted": "Подделка спуфа",
"loginProxy": "Прокси", "loginProxy": "Прокси",
"loginChangeServer": "Смена сервера",
"serverSettingsTitle": "Сервер",
"serverHostLabel": "Хост",
"serverPortLabel": "Порт",
"serverApply": "Применить и переподключиться",
"serverUseDefault": "Сбросить к умолчанию",
"serverInvalidHostOrPort": "Укажите корректный хост и порт (1–65535)",
"serverSettingsSaved": "Настройки сервера применены",
"serverReconnectFailed": "Не удалось подключиться к серверу",
"loginSignInWithQr": "По QR code", "loginSignInWithQr": "По QR code",
"loginSignInWithToken": "По токену", "loginSignInWithToken": "По токену",
"loginSignInWithSessionFile": "По файлу сессии", "loginSignInWithSessionFile": "По файлу сессии",
+8 -8
View File
@@ -21,10 +21,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -313,18 +313,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.19" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
material_symbols_icons: material_symbols_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -638,10 +638,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.7"
timezone: timezone:
dependency: "direct main" dependency: "direct main"
description: description: