188 lines
4.4 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|