feat(auth): server endpoint in bottom sheet on login
This commit is contained in:
@@ -70,7 +70,8 @@ class Api {
|
||||
});
|
||||
|
||||
try {
|
||||
await _connection.connect(ServerConfig.host, ServerConfig.port);
|
||||
final endpoint = await ServerConfig.loadEndpoint();
|
||||
await _connection.connect(endpoint.host, endpoint.port);
|
||||
} catch (e) {
|
||||
logger.e('Не удалось подключиться: $e');
|
||||
_cleanup();
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class ServerConfig {
|
||||
static const String host = 'api.oneme.ru';
|
||||
static const int port = 443;
|
||||
static const String defaultHost = 'api.oneme.ru';
|
||||
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 requestTimeout = Duration(seconds: 30);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:komet/l10n/terms_of_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'code_confirmation_screen.dart';
|
||||
import 'select_country_screen.dart';
|
||||
import 'server_settings_sheet.dart';
|
||||
import 'spoff_redacted_screen.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../../main.dart';
|
||||
@@ -448,6 +449,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
_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) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -457,7 +475,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (context) {
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -478,7 +496,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(sheetContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -498,7 +516,22 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@
|
||||
"loginReadTermsNotification": "Please read the terms of use first",
|
||||
"loginSpoofRedacted": "Spoof redaction",
|
||||
"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 (1–65535)",
|
||||
"serverSettingsSaved": "Server settings applied",
|
||||
"serverReconnectFailed": "Could not connect to the server",
|
||||
"loginSignInWithQr": "Sign in with QR code",
|
||||
"loginSignInWithToken": "Sign in with token",
|
||||
"loginSignInWithSessionFile": "Sign in with session file",
|
||||
|
||||
@@ -188,6 +188,60 @@ abstract class AppLocalizations {
|
||||
/// **'Proxy'**
|
||||
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 (1–65535)'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -54,6 +54,33 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
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 (1–65535)';
|
||||
|
||||
@override
|
||||
String get serverSettingsSaved => 'Server settings applied';
|
||||
|
||||
@override
|
||||
String get serverReconnectFailed => 'Could not connect to the server';
|
||||
|
||||
@override
|
||||
String get loginSignInWithQr => 'Sign in with QR code';
|
||||
|
||||
|
||||
@@ -55,6 +55,34 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get loginSignInWithQr => 'По QR code';
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
"loginReadTermsNotification": "Сначала прочитайте условия использования",
|
||||
"loginSpoofRedacted": "Подделка спуфа",
|
||||
"loginProxy": "Прокси",
|
||||
"loginChangeServer": "Смена сервера",
|
||||
"serverSettingsTitle": "Сервер",
|
||||
"serverHostLabel": "Хост",
|
||||
"serverPortLabel": "Порт",
|
||||
"serverApply": "Применить и переподключиться",
|
||||
"serverUseDefault": "Сбросить к умолчанию",
|
||||
"serverInvalidHostOrPort": "Укажите корректный хост и порт (1–65535)",
|
||||
"serverSettingsSaved": "Настройки сервера применены",
|
||||
"serverReconnectFailed": "Не удалось подключиться к серверу",
|
||||
"loginSignInWithQr": "По QR code",
|
||||
"loginSignInWithToken": "По токену",
|
||||
"loginSignInWithSessionFile": "По файлу сессии",
|
||||
|
||||
+8
-8
@@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -313,18 +313,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
version: "0.11.1"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -638,10 +638,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user