自定义线路与线路优化
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import '../core/constants/storage_keys.dart';
|
||||
import '../core/utils/app_logger.dart';
|
||||
import 'host_mapping_proxy_service.dart';
|
||||
import 'server_service.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
class CustomLineService {
|
||||
CustomLineService._();
|
||||
|
||||
static final CustomLineService instance = CustomLineService._();
|
||||
|
||||
HostMappingProxyEndpoint? get currentProxyEndpoint =>
|
||||
HostMappingProxyService.instance.endpoint;
|
||||
|
||||
Future<SelectedCustomLineNode?> getSelectedNode() async {
|
||||
final raw = await StorageService.instance.getString(
|
||||
StorageKeys.selectedCustomLineNode,
|
||||
);
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map<String, dynamic>) return null;
|
||||
final node = SelectedCustomLineNode.fromJson(decoded);
|
||||
if (!node.canRewriteDownloadUrl) return null;
|
||||
return node;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setSelectedNode(SelectedCustomLineNode node) async {
|
||||
if (!node.canRewriteDownloadUrl) {
|
||||
throw ArgumentError('The selected custom line node has no usable IP.');
|
||||
}
|
||||
await StorageService.instance.setString(
|
||||
StorageKeys.selectedCustomLineNode,
|
||||
jsonEncode(node.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearSelectedNode() async {
|
||||
await StorageService.instance.remove(StorageKeys.selectedCustomLineNode);
|
||||
await HostMappingProxyService.instance.stop();
|
||||
}
|
||||
|
||||
Future<CustomLineDownloadRoute> routeDownloadUrl(String url) async {
|
||||
if (!_canUseCustomLine()) {
|
||||
await HostMappingProxyService.instance.stop();
|
||||
return CustomLineDownloadRoute(url: url);
|
||||
}
|
||||
|
||||
final node = await getSelectedNode();
|
||||
if (node == null) return CustomLineDownloadRoute(url: url);
|
||||
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || uri.host.isEmpty) {
|
||||
return CustomLineDownloadRoute(url: url);
|
||||
}
|
||||
|
||||
final endpoint = await activateSelectedNodeForHost(uri.host);
|
||||
if (endpoint == null) {
|
||||
return CustomLineDownloadRoute(url: url);
|
||||
}
|
||||
|
||||
return CustomLineDownloadRoute(
|
||||
url: url,
|
||||
nodeName: node.name,
|
||||
originalHost: uri.host,
|
||||
proxy: endpoint,
|
||||
);
|
||||
}
|
||||
|
||||
Future<HostMappingProxyEndpoint?> activateSelectedNodeForHost(String host) {
|
||||
return activateSelectedNodeForHosts([host]);
|
||||
}
|
||||
|
||||
Future<HostMappingProxyEndpoint?> activateSelectedNodeForHosts(
|
||||
Iterable<String> hosts,
|
||||
) async {
|
||||
if (!_canUseCustomLine()) {
|
||||
await HostMappingProxyService.instance.stop();
|
||||
return null;
|
||||
}
|
||||
|
||||
final node = await getSelectedNode();
|
||||
if (node == null) {
|
||||
await HostMappingProxyService.instance.stop();
|
||||
return null;
|
||||
}
|
||||
|
||||
final hostMap = <String, String>{};
|
||||
void addHost(String value) {
|
||||
final host = value.trim();
|
||||
if (host.isNotEmpty) hostMap[host] = node.ip;
|
||||
}
|
||||
|
||||
addHost(node.host);
|
||||
for (final host in hosts) {
|
||||
addHost(host);
|
||||
}
|
||||
|
||||
if (hostMap.isEmpty) {
|
||||
await HostMappingProxyService.instance.stop();
|
||||
return null;
|
||||
}
|
||||
return HostMappingProxyService.instance.configure(hostMap);
|
||||
}
|
||||
|
||||
Future<HostMappingProxyEndpoint?> activateSelectedNodeForUrl(String url) {
|
||||
final uri = Uri.tryParse(url);
|
||||
return activateSelectedNodeForHost(uri?.host ?? '');
|
||||
}
|
||||
|
||||
Future<HostMappingProxyEndpoint?> activateSelectedNodeForUrls(
|
||||
Iterable<String> urls,
|
||||
) {
|
||||
final hosts = urls
|
||||
.map((url) => Uri.tryParse(url)?.host ?? '')
|
||||
.where((host) => host.trim().isNotEmpty);
|
||||
return activateSelectedNodeForHosts(hosts);
|
||||
}
|
||||
|
||||
Future<void> configureMediaPlayerProxy(Player player, String url) async {
|
||||
final endpoint = await activateSelectedNodeForUrl(url);
|
||||
final platform = player.platform;
|
||||
if (platform is! NativePlayer) return;
|
||||
|
||||
try {
|
||||
await platform.setProperty('http-proxy', endpoint?.proxyUri ?? '');
|
||||
AppLogger.d('媒体播放器自定义线路代理已配置: ${endpoint?.proxyUri ?? 'DIRECT'}');
|
||||
} catch (e) {
|
||||
AppLogger.d('媒体播放器自定义线路代理配置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
bool _canUseCustomLine() {
|
||||
final user = ServerService.instance.currentServer?.user;
|
||||
final groupName = user?.group?.name.trim().toLowerCase();
|
||||
return user != null &&
|
||||
groupName != null &&
|
||||
groupName.isNotEmpty &&
|
||||
groupName != 'user';
|
||||
}
|
||||
}
|
||||
|
||||
class SelectedCustomLineNode {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ip;
|
||||
final String host;
|
||||
final String protocol;
|
||||
final int? port;
|
||||
|
||||
const SelectedCustomLineNode({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ip,
|
||||
required this.host,
|
||||
this.protocol = '',
|
||||
this.port,
|
||||
});
|
||||
|
||||
bool get canRewriteDownloadUrl =>
|
||||
ip.trim().isNotEmpty && host.trim().isNotEmpty;
|
||||
|
||||
String routeScheme(String fallback) {
|
||||
final value = protocol.trim().toLowerCase();
|
||||
return value == 'http' || value == 'https' ? value : fallback;
|
||||
}
|
||||
|
||||
int? get routePort => port != null && port! > 0 ? port : null;
|
||||
|
||||
factory SelectedCustomLineNode.fromJson(Map<String, dynamic> json) {
|
||||
return SelectedCustomLineNode(
|
||||
id: json['id']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
ip: json['ip']?.toString() ?? '',
|
||||
host: json['host']?.toString() ?? '',
|
||||
protocol: json['protocol']?.toString() ?? '',
|
||||
port: _asNullableInt(json['port']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'ip': ip,
|
||||
'host': host,
|
||||
if (protocol.trim().isNotEmpty) 'protocol': protocol,
|
||||
if (routePort != null) 'port': routePort,
|
||||
};
|
||||
}
|
||||
|
||||
static int? _asNullableInt(Object? value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.round();
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
class CustomLineDownloadRoute {
|
||||
final String url;
|
||||
final Map<String, String> headers;
|
||||
final String? nodeName;
|
||||
final String? originalHost;
|
||||
final HostMappingProxyEndpoint? proxy;
|
||||
|
||||
const CustomLineDownloadRoute({
|
||||
required this.url,
|
||||
this.headers = const {},
|
||||
this.nodeName,
|
||||
this.originalHost,
|
||||
this.proxy,
|
||||
});
|
||||
|
||||
bool get changed => proxy != null || headers.isNotEmpty;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import '../core/constants/storage_keys.dart';
|
||||
import '../data/models/download_task_model.dart';
|
||||
import 'custom_line_service.dart';
|
||||
import 'file_service.dart';
|
||||
import 'storage_service.dart';
|
||||
import '../core/utils/app_logger.dart';
|
||||
@@ -261,7 +262,14 @@ class DownloadService {
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
return _startBdDownload(task, url, dir);
|
||||
final route = await CustomLineService.instance.routeDownloadUrl(url);
|
||||
if (route.changed) {
|
||||
AppLogger.d(
|
||||
'自定义线路下载代理已应用: node=${route.nodeName}, host=${route.originalHost}, proxy=${route.proxy?.proxyUri}',
|
||||
);
|
||||
}
|
||||
|
||||
return _startBdDownload(task, route.url, dir, headers: route.headers);
|
||||
} catch (e) {
|
||||
AppLogger.d('下载失败: $e');
|
||||
rethrow;
|
||||
@@ -272,12 +280,15 @@ class DownloadService {
|
||||
Future<String?> _startBdDownload(
|
||||
DownloadTaskModel task,
|
||||
String url,
|
||||
Directory dir,
|
||||
) async {
|
||||
Directory dir, {
|
||||
Map<String, String> headers = const {},
|
||||
}) async {
|
||||
await _configureCustomLineProxy();
|
||||
final wifiOnly = await isWifiOnlyEnabled();
|
||||
final retries = await getRetries();
|
||||
final bdTask = bd.DownloadTask(
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: task.fileName,
|
||||
directory: dir.path,
|
||||
baseDirectory: bd.BaseDirectory.root,
|
||||
@@ -340,10 +351,19 @@ class DownloadService {
|
||||
}
|
||||
|
||||
// 恢复任务:不删除部分文件,使用 resume 方式重建 bdTask
|
||||
final route = await CustomLineService.instance.routeDownloadUrl(url);
|
||||
if (route.changed) {
|
||||
AppLogger.d(
|
||||
'自定义线路下载代理已应用: node=${route.nodeName}, host=${route.originalHost}, proxy=${route.proxy?.proxyUri}',
|
||||
);
|
||||
}
|
||||
|
||||
await _configureCustomLineProxy();
|
||||
final wifiOnly = await isWifiOnlyEnabled();
|
||||
final retries = await getRetries();
|
||||
final bdTask = bd.DownloadTask(
|
||||
url: url,
|
||||
url: route.url,
|
||||
headers: route.headers,
|
||||
filename: task.fileName,
|
||||
directory: dir.path,
|
||||
baseDirectory: bd.BaseDirectory.root,
|
||||
@@ -382,6 +402,14 @@ class DownloadService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _configureCustomLineProxy() async {
|
||||
final endpoint = CustomLineService.instance.currentProxyEndpoint;
|
||||
final config = endpoint == null ? false : (endpoint.host, endpoint.port);
|
||||
await bd.FileDownloader().configure(
|
||||
globalConfig: (bd.Config.proxy, config),
|
||||
);
|
||||
}
|
||||
|
||||
/// 暂停下载
|
||||
Future<void> pauseDownload(String taskId) async {
|
||||
final bdTask = _bdTasks[taskId];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'api_service.dart';
|
||||
import '../core/utils/app_logger.dart';
|
||||
import '../core/utils/file_utils.dart';
|
||||
import 'custom_line_service.dart';
|
||||
|
||||
/// 文件服务
|
||||
class FileService {
|
||||
@@ -156,10 +157,25 @@ class FileService {
|
||||
data: data,
|
||||
headers: headers,
|
||||
);
|
||||
await _activateCustomLineForUrls(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> _activateCustomLineForUrls(Map<String, dynamic> response) async {
|
||||
final urls = response['urls'];
|
||||
if (urls is! List) return;
|
||||
|
||||
final resolvedUrls = <String>[];
|
||||
for (final item in urls) {
|
||||
if (item is! Map) continue;
|
||||
final url = item['url']?.toString();
|
||||
if (url == null || url.isEmpty) continue;
|
||||
resolvedUrls.add(url);
|
||||
}
|
||||
await CustomLineService.instance.activateSelectedNodeForUrls(resolvedUrls);
|
||||
}
|
||||
|
||||
/// 创建直接链接(分享链接)
|
||||
Future<List<Map<String, dynamic>>> createDirectLinks({
|
||||
required List<String> uris,
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../core/utils/app_logger.dart';
|
||||
|
||||
class HostMappingProxyService {
|
||||
HostMappingProxyService._();
|
||||
|
||||
static final HostMappingProxyService instance = HostMappingProxyService._();
|
||||
|
||||
ServerSocket? _server;
|
||||
HostMappingProxyEndpoint? _endpoint;
|
||||
Map<String, String> _hostMap = const {};
|
||||
|
||||
HostMappingProxyEndpoint? get endpoint => _hostMap.isEmpty ? null : _endpoint;
|
||||
|
||||
Future<HostMappingProxyEndpoint?> configure(
|
||||
Map<String, String> hostMap,
|
||||
) async {
|
||||
final cleaned = <String, String>{};
|
||||
for (final entry in hostMap.entries) {
|
||||
final host = _normalizeHost(entry.key);
|
||||
final ip = entry.value.trim();
|
||||
if (host.isNotEmpty && ip.isNotEmpty) {
|
||||
cleaned[host] = ip;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned.isEmpty) {
|
||||
await stop();
|
||||
return null;
|
||||
}
|
||||
|
||||
_hostMap = cleaned;
|
||||
if (_server == null) {
|
||||
_server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
_endpoint = HostMappingProxyEndpoint(
|
||||
host: InternetAddress.loopbackIPv4.address,
|
||||
port: _server!.port,
|
||||
);
|
||||
_server!.listen(
|
||||
_handleClient,
|
||||
onError: (e) {
|
||||
AppLogger.d('自定义线路本地代理监听错误: $e');
|
||||
},
|
||||
);
|
||||
AppLogger.d('自定义线路本地代理已启动: ${_endpoint!.proxyUri}');
|
||||
}
|
||||
return _endpoint;
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_hostMap = const {};
|
||||
final server = _server;
|
||||
_server = null;
|
||||
_endpoint = null;
|
||||
if (server != null) {
|
||||
await server.close();
|
||||
AppLogger.d('自定义线路本地代理已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
void _handleClient(Socket client) {
|
||||
unawaited(_serveClient(client));
|
||||
}
|
||||
|
||||
Future<void> _serveClient(Socket client) async {
|
||||
Socket? remote;
|
||||
StreamController<List<int>>? rest;
|
||||
try {
|
||||
client.setOption(SocketOption.tcpNoDelay, true);
|
||||
final initial = await _readInitialRequest(client);
|
||||
rest = initial.rest;
|
||||
final lines = latin1.decode(initial.header).split('\r\n');
|
||||
if (lines.isEmpty || lines.first.trim().isEmpty) {
|
||||
throw const FormatException('empty proxy request');
|
||||
}
|
||||
|
||||
final first = lines.first.split(' ');
|
||||
if (first.length < 3) {
|
||||
throw FormatException('invalid proxy request: ${lines.first}');
|
||||
}
|
||||
|
||||
final method = first[0].toUpperCase();
|
||||
if (method == 'CONNECT') {
|
||||
final target = _splitAuthority(first[1], 443);
|
||||
remote = await _connectMapped(target.host, target.port);
|
||||
client.add(utf8.encode('HTTP/1.1 200 Connection Established\r\n\r\n'));
|
||||
await client.flush();
|
||||
} else {
|
||||
final target = _httpTarget(first[1], lines);
|
||||
remote = await _connectMapped(target.host, target.port);
|
||||
remote.add(_rewriteHttpHeader(initial.header, target.requestTarget));
|
||||
}
|
||||
|
||||
remote.setOption(SocketOption.tcpNoDelay, true);
|
||||
unawaited(rest.stream.pipe(remote).catchError((_) {}));
|
||||
unawaited(remote.cast<List<int>>().pipe(client).catchError((_) {}));
|
||||
} catch (e) {
|
||||
AppLogger.d('自定义线路本地代理请求失败: $e');
|
||||
try {
|
||||
client.add(utf8.encode('HTTP/1.1 502 Bad Gateway\r\n\r\n'));
|
||||
await client.flush();
|
||||
} catch (_) {}
|
||||
await rest?.close();
|
||||
remote?.destroy();
|
||||
client.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Future<_ProxyInitialRequest> _readInitialRequest(Socket socket) {
|
||||
final completer = Completer<_ProxyInitialRequest>();
|
||||
final builder = BytesBuilder(copy: false);
|
||||
final rest = StreamController<List<int>>(sync: true);
|
||||
late StreamSubscription<Uint8List> subscription;
|
||||
|
||||
subscription = socket.listen(
|
||||
(chunk) {
|
||||
if (completer.isCompleted) {
|
||||
rest.add(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.add(chunk);
|
||||
final bytes = builder.toBytes();
|
||||
final end = _headerEnd(bytes);
|
||||
if (end >= 0) {
|
||||
final headerEnd = end + 4;
|
||||
completer.complete(
|
||||
_ProxyInitialRequest(
|
||||
header: bytes.sublist(0, headerEnd),
|
||||
rest: rest,
|
||||
),
|
||||
);
|
||||
if (bytes.length > headerEnd) {
|
||||
rest.add(bytes.sublist(headerEnd));
|
||||
}
|
||||
} else if (bytes.length > 64 * 1024) {
|
||||
completer.completeError(
|
||||
const FormatException('proxy request header too large'),
|
||||
);
|
||||
unawaited(subscription.cancel());
|
||||
unawaited(rest.close());
|
||||
}
|
||||
},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stackTrace);
|
||||
} else {
|
||||
rest.addError(error, stackTrace);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(const SocketException('proxy client closed'));
|
||||
}
|
||||
unawaited(rest.close());
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
int _headerEnd(List<int> bytes) {
|
||||
for (var i = 0; i <= bytes.length - 4; i++) {
|
||||
if (bytes[i] == 13 &&
|
||||
bytes[i + 1] == 10 &&
|
||||
bytes[i + 2] == 13 &&
|
||||
bytes[i + 3] == 10) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Future<Socket> _connectMapped(String host, int port) {
|
||||
final normalized = _normalizeHost(host);
|
||||
final targetHost = _hostMap[normalized] ?? host;
|
||||
return Socket.connect(
|
||||
targetHost,
|
||||
port,
|
||||
timeout: const Duration(seconds: 12),
|
||||
);
|
||||
}
|
||||
|
||||
_ProxyTarget _httpTarget(String target, List<String> lines) {
|
||||
final uri = Uri.tryParse(target);
|
||||
if (uri != null && uri.hasScheme && uri.host.isNotEmpty) {
|
||||
return _ProxyTarget(
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : (uri.scheme == 'https' ? 443 : 80),
|
||||
requestTarget: _originForm(uri),
|
||||
);
|
||||
}
|
||||
|
||||
final hostHeader = _headerValue(lines, 'Host');
|
||||
final authority = _splitAuthority(hostHeader, 80);
|
||||
return _ProxyTarget(
|
||||
host: authority.host,
|
||||
port: authority.port,
|
||||
requestTarget: target,
|
||||
);
|
||||
}
|
||||
|
||||
List<int> _rewriteHttpHeader(List<int> header, String requestTarget) {
|
||||
final text = latin1.decode(header);
|
||||
final lines = text.split('\r\n');
|
||||
final first = lines.first.split(' ');
|
||||
first[1] = requestTarget.isEmpty ? '/' : requestTarget;
|
||||
lines[0] = first.join(' ');
|
||||
final filtered = lines
|
||||
.where((line) => !line.toLowerCase().startsWith('proxy-connection:'))
|
||||
.join('\r\n');
|
||||
return latin1.encode(filtered);
|
||||
}
|
||||
|
||||
String _originForm(Uri uri) {
|
||||
final path = uri.path.isEmpty ? '/' : uri.path;
|
||||
return uri.hasQuery ? '$path?${uri.query}' : path;
|
||||
}
|
||||
|
||||
String _headerValue(List<String> lines, String name) {
|
||||
final prefix = '${name.toLowerCase()}:';
|
||||
for (final line in lines.skip(1)) {
|
||||
if (line.toLowerCase().startsWith(prefix)) {
|
||||
return line.substring(line.indexOf(':') + 1).trim();
|
||||
}
|
||||
}
|
||||
throw FormatException('missing $name header');
|
||||
}
|
||||
|
||||
_ProxyAuthority _splitAuthority(String value, int defaultPort) {
|
||||
final authority = value.trim();
|
||||
if (authority.startsWith('[')) {
|
||||
final end = authority.indexOf(']');
|
||||
if (end > 0) {
|
||||
final host = authority.substring(1, end);
|
||||
final port = authority.length > end + 2
|
||||
? int.tryParse(authority.substring(end + 2)) ?? defaultPort
|
||||
: defaultPort;
|
||||
return _ProxyAuthority(host, port);
|
||||
}
|
||||
}
|
||||
|
||||
final colon = authority.lastIndexOf(':');
|
||||
if (colon > 0 && colon < authority.length - 1) {
|
||||
final port = int.tryParse(authority.substring(colon + 1));
|
||||
if (port != null) {
|
||||
return _ProxyAuthority(authority.substring(0, colon), port);
|
||||
}
|
||||
}
|
||||
return _ProxyAuthority(authority, defaultPort);
|
||||
}
|
||||
|
||||
String _normalizeHost(String host) {
|
||||
final value = host.trim();
|
||||
final uri = Uri.tryParse(value);
|
||||
final resolved = uri != null && uri.hasScheme && uri.host.isNotEmpty
|
||||
? uri.host
|
||||
: value;
|
||||
return resolved.toLowerCase().replaceAll(RegExp(r'\.$'), '');
|
||||
}
|
||||
}
|
||||
|
||||
class HostMappingProxyEndpoint {
|
||||
final String host;
|
||||
final int port;
|
||||
|
||||
const HostMappingProxyEndpoint({required this.host, required this.port});
|
||||
|
||||
String get proxyUri => 'http://$host:$port';
|
||||
}
|
||||
|
||||
class HostMappingHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.findProxy = (_) {
|
||||
final endpoint = HostMappingProxyService.instance.endpoint;
|
||||
if (endpoint == null) return 'DIRECT';
|
||||
return 'PROXY ${endpoint.host}:${endpoint.port}; DIRECT';
|
||||
};
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
class _ProxyInitialRequest {
|
||||
final Uint8List header;
|
||||
final StreamController<List<int>> rest;
|
||||
|
||||
const _ProxyInitialRequest({required this.header, required this.rest});
|
||||
}
|
||||
|
||||
class _ProxyAuthority {
|
||||
final String host;
|
||||
final int port;
|
||||
|
||||
const _ProxyAuthority(this.host, this.port);
|
||||
}
|
||||
|
||||
class _ProxyTarget {
|
||||
final String host;
|
||||
final int port;
|
||||
final String requestTarget;
|
||||
|
||||
const _ProxyTarget({
|
||||
required this.host,
|
||||
required this.port,
|
||||
required this.requestTarget,
|
||||
});
|
||||
}
|
||||
@@ -45,12 +45,7 @@ class ServerService {
|
||||
} catch (e) {
|
||||
AppLogger.d('加载服务器列表失败: $e');
|
||||
// 加载失败时使用默认服务器
|
||||
_servers = [
|
||||
ServerModel(
|
||||
label: _defaultLabel,
|
||||
baseUrl: _defaultBaseUrl,
|
||||
),
|
||||
];
|
||||
_servers = [ServerModel(label: _defaultLabel, baseUrl: _defaultBaseUrl)];
|
||||
_currentServer = _servers.first;
|
||||
}
|
||||
}
|
||||
@@ -65,15 +60,10 @@ class ServerService {
|
||||
}
|
||||
}
|
||||
|
||||
_currentServer = (savedDefaultServer ??
|
||||
ServerModel(
|
||||
label: _defaultLabel,
|
||||
baseUrl: _defaultBaseUrl,
|
||||
))
|
||||
.copyWith(
|
||||
label: _defaultLabel,
|
||||
baseUrl: _defaultBaseUrl,
|
||||
);
|
||||
_currentServer =
|
||||
(savedDefaultServer ??
|
||||
ServerModel(label: _defaultLabel, baseUrl: _defaultBaseUrl))
|
||||
.copyWith(label: _defaultLabel, baseUrl: _defaultBaseUrl);
|
||||
|
||||
_servers = [_currentServer!];
|
||||
}
|
||||
@@ -91,7 +81,9 @@ class ServerService {
|
||||
/// 保存上次选中的服务器
|
||||
Future<void> _saveLastSelected() async {
|
||||
if (_currentServer != null) {
|
||||
await StorageService.instance.setLastSelectedServerLabel(_currentServer!.label);
|
||||
await StorageService.instance.setLastSelectedServerLabel(
|
||||
_currentServer!.label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +117,21 @@ class ServerService {
|
||||
String? password,
|
||||
UserModel? user,
|
||||
bool? rememberMe,
|
||||
bool clearEmail = false,
|
||||
bool clearPassword = false,
|
||||
bool clearUser = false,
|
||||
}) async {
|
||||
if (_currentServer == null) {
|
||||
throw Exception('没有选中的服务器');
|
||||
}
|
||||
|
||||
_currentServer = _currentServer!.copyWith(
|
||||
email: email,
|
||||
password: password,
|
||||
user: user,
|
||||
_currentServer = ServerModel(
|
||||
label: _currentServer!.label,
|
||||
baseUrl: _currentServer!.baseUrl,
|
||||
rememberMe: rememberMe ?? _currentServer!.rememberMe,
|
||||
email: clearEmail ? null : (email ?? _currentServer!.email),
|
||||
password: clearPassword ? null : (password ?? _currentServer!.password),
|
||||
user: clearUser ? null : (user ?? _currentServer!.user),
|
||||
);
|
||||
|
||||
// 更新列表中的引用
|
||||
@@ -152,17 +149,15 @@ class ServerService {
|
||||
email: null,
|
||||
password: null,
|
||||
user: null,
|
||||
clearEmail: true,
|
||||
clearPassword: true,
|
||||
clearUser: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置为默认服务器列表
|
||||
Future<void> resetToDefault() async {
|
||||
_servers = [
|
||||
ServerModel(
|
||||
label: _defaultLabel,
|
||||
baseUrl: _defaultBaseUrl,
|
||||
),
|
||||
];
|
||||
_servers = [ServerModel(label: _defaultLabel, baseUrl: _defaultBaseUrl)];
|
||||
_currentServer = _servers.first;
|
||||
await _saveServers();
|
||||
await _saveLastSelected();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'api_service.dart';
|
||||
import '../core/utils/app_logger.dart';
|
||||
import '../core/utils/file_utils.dart';
|
||||
import '../core/utils/time_flow_decoder.dart';
|
||||
import 'custom_line_service.dart';
|
||||
|
||||
/// 缩略图缓存条目
|
||||
class _ThumbCacheEntry {
|
||||
@@ -41,6 +42,9 @@ class ThumbnailService {
|
||||
// 1. 检查内存缓存
|
||||
final cached = _urlCache[cacheKey];
|
||||
if (cached != null && !cached.isExpired) {
|
||||
await CustomLineService.instance.activateSelectedNodeForUrl(
|
||||
cached.imageUrl,
|
||||
);
|
||||
return cached.imageUrl;
|
||||
}
|
||||
if (cached != null) {
|
||||
@@ -64,7 +68,10 @@ class ThumbnailService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _fetchThumbnailUrl(String fileUri, String? contextHint) async {
|
||||
Future<String?> _fetchThumbnailUrl(
|
||||
String fileUri,
|
||||
String? contextHint,
|
||||
) async {
|
||||
try {
|
||||
final uri = FileUtils.toCloudreveUri(fileUri);
|
||||
final headers = contextHint != null
|
||||
@@ -97,12 +104,15 @@ class ThumbnailService {
|
||||
if (url.isEmpty) return null;
|
||||
|
||||
AppLogger.d('ThumbnailService: resolved URL for $fileUri = $url');
|
||||
await CustomLineService.instance.activateSelectedNodeForUrl(url);
|
||||
|
||||
// 解析过期时间,缓存提前 30 秒过期
|
||||
DateTime expiresAt;
|
||||
if (expiresStr != null) {
|
||||
try {
|
||||
expiresAt = DateTime.parse(expiresStr).subtract(const Duration(seconds: 30));
|
||||
expiresAt = DateTime.parse(
|
||||
expiresStr,
|
||||
).subtract(const Duration(seconds: 30));
|
||||
} catch (_) {
|
||||
expiresAt = DateTime.now().add(const Duration(minutes: 5));
|
||||
}
|
||||
@@ -118,7 +128,9 @@ class ThumbnailService {
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
AppLogger.d('ThumbnailService: failed to get thumbnail URL for $fileUri: $e');
|
||||
AppLogger.d(
|
||||
'ThumbnailService: failed to get thumbnail URL for $fileUri: $e',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user