Files
Qlyra/lib/frontend/screens/auth/proxy_settings_sheet.dart
T

267 lines
9.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:komet/backend/api.dart';
import 'package:komet/core/config/proxy_config.dart';
import 'package:komet/l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/labeled_settings_field.dart';
import '../../widgets/sheet_helpers.dart';
class ProxySettingsSheet extends StatefulWidget {
const ProxySettingsSheet({super.key});
@override
State<ProxySettingsSheet> createState() => _ProxySettingsSheetState();
}
class _ProxySettingsSheetState extends State<ProxySettingsSheet> {
final _hostController = TextEditingController();
final _portController = TextEditingController(text: '1080');
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
ProxyType _selectedType = ProxyType.none;
ProxySettings _applied = const ProxySettings();
bool _busy = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final settings = await ProxyConfig.load();
if (!mounted) return;
setState(() {
_applied = settings;
_selectedType = settings.type;
_hostController.text = settings.host;
_portController.text = '${settings.port}';
_usernameController.text = settings.username ?? '';
_passwordController.text = settings.password ?? '';
});
}
Future<void> _apply(AppLocalizations l10n) async {
if (_selectedType == ProxyType.none) {
return _disable(l10n);
}
final host = _hostController.text.trim();
final port = int.tryParse(_portController.text.trim());
if (host.isEmpty || port == null || port < 1 || port > 65535) {
showCustomNotification(context, l10n.proxyInvalidHostOrPort);
return;
}
setState(() => _busy = true);
try {
final username = _usernameController.text.trim();
final password = _passwordController.text.trim();
final settings = ProxySettings(
type: _selectedType,
host: host,
port: port,
username: username.isNotEmpty ? username : null,
password: password.isNotEmpty ? password : null,
);
await ProxyConfig.save(settings);
await api.disconnect();
await api.connect();
if (!mounted) return;
setState(() => _applied = settings);
if (api.state == SessionState.online) {
showCustomNotification(context, l10n.proxySettingsSaved);
} else {
showCustomNotification(context, l10n.serverReconnectFailed);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _disable(AppLocalizations l10n) async {
setState(() => _busy = true);
try {
await ProxyConfig.clear();
setState(() {
_selectedType = ProxyType.none;
_applied = const ProxySettings();
});
await api.disconnect();
await api.connect();
if (!mounted) return;
if (api.state == SessionState.online) {
showCustomNotification(context, l10n.proxySettingsSaved);
} else {
showCustomNotification(context, l10n.serverReconnectFailed);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
void dispose() {
_hostController.dispose();
_portController.dispose();
_usernameController.dispose();
_passwordController.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;
final isActive = _selectedType != ProxyType.none;
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: [
const Center(
child: SheetGrabber(margin: EdgeInsets.only(bottom: 16)),
),
Text(
l10n.proxySettingsTitle,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l10n.proxyCurrentState(_appliedLabel(l10n)),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 20),
// Proxy type selector
_buildTypeSelector(cs, l10n),
const SizedBox(height: 16),
// Fields shown only when proxy is enabled
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: isActive
? Column(
children: [
LabeledSettingsField(
controller: _hostController,
label: l10n.proxyHostLabel,
hintText: '127.0.0.1',
keyboardType: TextInputType.url,
enabled: !_busy,
),
const SizedBox(height: 16),
LabeledSettingsField(
controller: _portController,
label: l10n.proxyPortLabel,
hintText: _selectedType == ProxyType.socks5
? '1080'
: '8080',
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
enabled: !_busy,
),
const SizedBox(height: 16),
LabeledSettingsField(
controller: _usernameController,
label: l10n.proxyUsernameLabel,
keyboardType: TextInputType.text,
enabled: !_busy,
),
const SizedBox(height: 16),
LabeledSettingsField(
controller: _passwordController,
label: l10n.proxyPasswordLabel,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
enabled: !_busy,
),
const SizedBox(height: 8),
],
)
: const SizedBox.shrink(),
),
const SizedBox(height: 16),
FilledButton(
onPressed: (_busy || !(isActive || _applied.isEnabled))
? null
: () => _apply(l10n),
child: Text(isActive ? l10n.proxyApply : l10n.proxyDisable),
),
],
),
),
),
);
}
String _appliedLabel(AppLocalizations l10n) {
if (!_applied.isEnabled) return l10n.proxyTypeNone;
final type = _applied.type == ProxyType.socks5
? l10n.proxyTypeSocks5
: l10n.proxyTypeHttp;
return '$type · ${_applied.host}:${_applied.port}';
}
Widget _buildTypeSelector(ColorScheme cs, AppLocalizations l10n) {
final labels = {
ProxyType.none: l10n.proxyTypeNone,
ProxyType.socks5: l10n.proxyTypeSocks5,
ProxyType.httpConnect: l10n.proxyTypeHttp,
};
return Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(4),
child: Row(
children: ProxyType.values.map((type) {
final selected = _selectedType == type;
return Expanded(
child: GestureDetector(
onTap: _busy ? null : () => setState(() => _selectedType = type),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: selected ? cs.primary : Colors.transparent,
borderRadius: BorderRadius.circular(9),
),
alignment: Alignment.center,
child: Text(
labels[type]!,
style: TextStyle(
color: selected ? cs.onPrimary : cs.onSurfaceVariant,
fontSize: 13,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
),
),
),
),
);
}).toList(),
),
);
}
}