Files
vpn/lib/services/vpn_service.dart
2026-04-05 11:54:37 +03:00

188 lines
4.4 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_v2ray_client/flutter_v2ray.dart';
import 'package:vpn/models/storage_model.dart';
final class VpnService extends ChangeNotifier {
static const MethodChannel _quickTileChannel = MethodChannel(
'vpn/quick_tile',
);
VpnService() {
_v2ray = V2ray(onStatusChanged: _handleStatusChanged);
}
late final V2ray _v2ray;
bool _isInitialized = false;
bool _isProcessing = false;
bool _isConnected = false;
String _connectionState = 'DISCONNECTED';
bool get isProcessing => _isProcessing;
bool get isConnected => _isConnected;
String get connectionState => _connectionState;
Future<String?> init() async {
if (_isInitialized) return null;
try {
await _v2ray.initialize();
_isInitialized = true;
return null;
} catch (e) {
return 'Init error: $e';
}
}
Future<String?> toggle(StorageModel model) async {
if (_isProcessing) return null;
if (_isConnected) {
return stop();
}
final String link = _selectedConfig(model).trim();
if (link.isEmpty) {
return 'Config link is empty';
}
return start(link);
}
Future<String?> start(String link) async {
if (_isProcessing) return null;
_isProcessing = true;
notifyListeners();
try {
final String? initError = await init();
if (initError != null) {
return initError;
}
final String normalizedLink = link.trim();
if (normalizedLink.isEmpty) {
return 'Config link is empty';
}
final parsed = V2ray.parseFromURL(normalizedLink);
final granted = await _v2ray.requestPermission();
if (!granted) {
return 'VPN permission denied';
}
await _v2ray.startV2Ray(
remark: parsed.remark.isEmpty ? 'VPN' : parsed.remark,
config: parsed.getFullConfiguration(),
proxyOnly: false,
);
_connectionState = 'CONNECTING';
notifyListeners();
return null;
} catch (e) {
return 'Error: $e';
} finally {
_isProcessing = false;
notifyListeners();
}
}
Future<String?> stop() async {
if (_isProcessing) return null;
_isProcessing = true;
notifyListeners();
try {
await _v2ray.stopV2Ray();
_connectionState = 'DISCONNECTED';
_isConnected = false;
return null;
} catch (e) {
return 'Error: $e';
} finally {
_isProcessing = false;
notifyListeners();
}
}
String _selectedConfig(StorageModel model) {
final int index = model.selected;
if (index < 0 || index >= model.configs.length) {
return '';
}
return model.configs[index].config;
}
void _handleStatusChanged(V2RayStatus status) {
_connectionState = status.state;
_isConnected = status.state == 'CONNECTED';
_isProcessing = false;
notifyListeners();
}
String? getName(String config) {
final String link = config.trim();
if (link.isEmpty) {
return null;
}
try {
final parsed = V2ray.parseFromURL(link);
if (parsed.remark.trim().isNotEmpty) {
return parsed.remark.trim();
} else {
return null;
}
} catch (_) {
return null;
}
}
Future<void> syncActiveConfig(StorageModel model) async {
final Map<String, dynamic>? payload = _buildActiveConfigPayload(model);
try {
if (payload == null) {
await _quickTileChannel.invokeMethod<void>('clearActiveConfig');
} else {
await _quickTileChannel.invokeMethod<void>('setActiveConfig', payload);
}
await _quickTileChannel.invokeMethod<void>('refreshTile');
} catch (_) {
// Quick settings integration is optional; ignore platform failures.
}
}
Map<String, dynamic>? _buildActiveConfigPayload(StorageModel model) {
final int index = model.selected;
if (index < 0 || index >= model.configs.length) {
return null;
}
final String link = model.configs[index].config.trim();
if (link.isEmpty) {
return null;
}
try {
final parsed = V2ray.parseFromURL(link);
final String remark = parsed.remark.trim();
if (remark.isEmpty) {
return null;
}
return <String, dynamic>{
'remark': remark,
'config': parsed.getFullConfiguration(),
'proxyOnly': false,
};
} catch (_) {
return null;
}
}
}