import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:http/http.dart' as http; import '../models/api_response.dart'; import '../models/ble_status.dart'; import '../models/player_status.dart'; import '../models/video_item.dart'; import '../models/wifi_network.dart'; import '../models/wifi_status.dart'; class HttpApiService { HttpApiService({required String baseUrl, http.Client? client}) : _baseUrl = _normalizeBaseUrl(baseUrl), _client = client ?? http.Client(); final http.Client _client; String _baseUrl; String get baseUrl => _baseUrl; set baseUrl(String value) { _baseUrl = _normalizeBaseUrl(value); } Uri _uri(String path, [Map? queryParameters]) { final normalizedPath = path.startsWith('/') ? path : '/$path'; return Uri.parse('$_baseUrl$normalizedPath').replace( queryParameters: queryParameters == null || queryParameters.isEmpty ? null : queryParameters, ); } Future fetchConsolePage() async { final response = await _client.get(_uri('/')); _ensureSuccess(response); return response.body; } Future fetchConsoleIndexHtml() async { final response = await _client.get(_uri('/index.html')); _ensureSuccess(response); return response.body; } Future getStatus() => getPlaybackStatus(); Future getPlaybackStatus() async { final response = await _client.get(_uri('/api/status')); _ensureSuccess(response); return PlayerStatus.fromJson(_decodeMap(response.body)); } Future play() => _postCommand('/api/play'); Future pause() => _postCommand('/api/pause'); Future next() => _postCommand('/api/next'); Future previous() => _postCommand('/api/previous'); Future goto(int index) => _postCommand('/api/goto/$index'); Future gotoIndex(int index) => goto(index); Future> getPlaylist() async { final response = await _client.get(_uri('/api/playlist')); _ensureSuccess(response); final decoded = _decodeJson(response.body); if (decoded is! List) { return const []; } return decoded.map((dynamic item) { if (item is String) { return item; } if (item is Map) { return item['name']?.toString() ?? jsonEncode(item); } if (item is Map) { return item['name']?.toString() ?? jsonEncode(item); } return item.toString(); }).toList(growable: false); } Future changeScene(String name) { return _postCommand('/api/scene/${Uri.encodeComponent(name)}'); } Future switchScene(String name) => changeScene(name); Future trigger(String name, [String? value]) { final suffix = value == null || value.isEmpty ? '' : '/${Uri.encodeComponent(value)}'; return _postCommand('/api/trigger/${Uri.encodeComponent(name)}$suffix'); } Future triggerEvent(String name, {String? value}) { return trigger(name, value); } Future getWifiStatus() async { final response = await _client.get(_uri('/api/wifi/status')); _ensureSuccess(response); return WifiStatus.fromJson(_decodeMap(response.body)); } Future> scanWifi() async { try { final response = await _client.get(_uri('/api/wifi/scan')); _ensureSuccess(response); return _decodeWifiNetworks(response.body); } on ApiException { final response = await _client.post(_uri('/api/wifi/scan')); _ensureSuccess(response); return _decodeWifiNetworks(response.body); } } Future> scanWifiViaGet() => scanWifi(); Future> scanWifiViaPost() => scanWifi(); Future connectWifi(String ssid, String password) async { final response = await _client.post( _uri('/api/wifi/connect'), headers: _jsonHeaders, body: jsonEncode({ 'ssid': ssid, 'password': password, }), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future startAP([String? ssid, String? password]) async { final body = { if (ssid != null && ssid.isNotEmpty) 'ssid': ssid, if (password != null && password.isNotEmpty) 'password': password, }; try { final response = await _client.post( _uri('/api/wifi/ap/start'), headers: _jsonHeaders, body: jsonEncode(body), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } on ApiException { final response = await _client.post( _uri('/api/wifi/hotspot/start'), headers: _jsonHeaders, body: jsonEncode(body), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } } Future startAccessPoint({String? ssid, String? password}) { return startAP(ssid, password); } Future startHotspot({String? ssid, String? password}) { return startAP(ssid, password); } Future stopAP() async { try { return await _postCommand('/api/wifi/ap/stop'); } on ApiException { return _postCommand('/api/wifi/hotspot/stop'); } } Future stopAccessPoint() => stopAP(); Future stopHotspot() => stopAP(); Future getBleStatus() async { final response = await _client.get(_uri('/api/ble/status')); _ensureSuccess(response); return BleServiceStatus.fromJson(_decodeMap(response.body)); } Future startBle([String? deviceName]) async { final response = await _client.post( _uri('/api/ble/start'), headers: _jsonHeaders, body: jsonEncode({ if (deviceName != null && deviceName.isNotEmpty) 'device_name': deviceName, }), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future stopBle() => _postCommand('/api/ble/stop'); Future> getVideos() async { final response = await _client.get(_uri('/api/videos')); _ensureSuccess(response); final decoded = _decodeJson(response.body); if (decoded is! List) { return const []; } return decoded.map((dynamic item) { if (item is Map) { return VideoItem.fromJson(item); } if (item is Map) { return VideoItem.fromJson(Map.from(item)); } return const VideoItem(name: '', size: 0); }).where((item) => item.name.isNotEmpty).toList(growable: false); } Future uploadVideo(File file) { return _uploadSingleFile('/api/videos/upload', file); } Future uploadVideos(List filePaths) async { if (filePaths.isEmpty) { throw const ApiException('未选择上传文件'); } final request = http.MultipartRequest('POST', _uri('/api/videos/upload')); for (final filePath in filePaths) { request.files.add(await http.MultipartFile.fromPath('file', filePath)); } final response = await http.Response.fromStream(await request.send()); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future deleteVideo(String name) { return _deleteCommand('/api/videos/${Uri.encodeComponent(name)}'); } Future> getConfig() async { final response = await _client.get(_uri('/api/config')); _ensureSuccess(response); return _decodeMap(response.body); } Future updateConfig(Map config) async { final response = await _client.post( _uri('/api/config'), headers: _jsonHeaders, body: jsonEncode(config), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future> getDisplayConfig() async { final response = await _client.get(_uri('/api/config/display')); _ensureSuccess(response); return _decodeMap(response.body); } Future> getAvailableConfigs() async { final response = await _client.get(_uri('/api/config/available')); _ensureSuccess(response); return _decodeMap(response.body); } Future switchConfig(String filename) async { final response = await _client.post( _uri('/api/config/switch'), headers: _jsonHeaders, body: jsonEncode({'filename': filename}), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future>> listFiles(String dirKey, [String? path]) async { final response = await _client.get( _uri('/api/files/$dirKey', _pathQuery(path)), ); _ensureSuccess(response); final decoded = _decodeJson(response.body); if (decoded is! List) { return const >[]; } return decoded .whereType() .map((entry) => Map.from(entry)) .toList(growable: false); } Future uploadFile(String dirKey, File file, [String? path]) { return _uploadSingleFile( '/api/files/$dirKey/upload', file, directoryPath: path, ); } Future downloadFile(String dirKey, String path) async { final response = await _client.get( _uri('/api/files/$dirKey/download', {'path': path}), ); _ensureSuccess(response, expectJson: false); return response.bodyBytes; } Future deleteFile(String dirKey, String path) async { final response = await _client.post( _uri('/api/files/$dirKey/delete'), headers: _jsonHeaders, body: jsonEncode({'path': path}), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future mkdir(String dirKey, String path) async { final response = await _client.post( _uri('/api/files/$dirKey/mkdir'), headers: _jsonHeaders, body: jsonEncode({'path': path}), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future>> getPlugins() async { final response = await _client.get(_uri('/api/plugins')); _ensureSuccess(response); final decoded = _decodeJson(response.body); if (decoded is! List) { return const >[]; } return decoded .whereType() .map((plugin) => Map.from(plugin)) .toList(growable: false); } Future enablePlugin(String id) { return _postCommand('/api/plugins/${Uri.encodeComponent(id)}/enable'); } Future disablePlugin(String id) { return _postCommand('/api/plugins/${Uri.encodeComponent(id)}/disable'); } Future installPlugin(String id, [String? version]) async { final response = await _client.post( _uri('/api/plugins/install'), headers: _jsonHeaders, body: jsonEncode({ 'id': id, if (version != null && version.isNotEmpty) 'version': version, }), ); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future _uploadSingleFile( String endpoint, File file, { String? directoryPath, }) async { final request = http.MultipartRequest( 'POST', _uri(endpoint, _pathQuery(directoryPath)), ); request.files.add(await http.MultipartFile.fromPath('file', file.path)); final response = await http.Response.fromStream(await request.send()); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future _postCommand(String path) async { final response = await _client.post(_uri(path)); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } Future _deleteCommand(String path) async { final response = await _client.delete(_uri(path)); _ensureSuccess(response); return ApiResponse.fromJson(_decodeMap(response.body)); } List _decodeWifiNetworks(String body) { final decoded = _decodeJson(body); if (decoded is! List) { return const []; } return decoded.map((dynamic item) { if (item is Map) { return WifiNetwork.fromJson(item); } if (item is Map) { return WifiNetwork.fromJson(Map.from(item)); } return const WifiNetwork(ssid: '', signal: 0, security: 'Unknown'); }).where((network) => network.ssid.isNotEmpty).toList(growable: false); } dynamic _decodeJson(String body) { try { return jsonDecode(body); } on FormatException catch (error) { throw ApiException('JSON 解析失败: ${error.message}'); } } Map _decodeMap(String body) { final decoded = _decodeJson(body); if (decoded is Map) { return decoded; } if (decoded is Map) { return Map.from(decoded); } throw const ApiException('期望返回 JSON 对象'); } Map? _pathQuery(String? path) { if (path == null || path.isEmpty) { return null; } return {'path': path}; } void _ensureSuccess(http.Response response, {bool expectJson = true}) { if (response.statusCode >= 200 && response.statusCode < 300) { return; } if (!expectJson) { throw ApiException( '请求失败 (${response.statusCode}): ${response.body}', statusCode: response.statusCode, ); } String message = response.body; try { final decoded = _decodeJson(response.body); if (decoded is Map) { message = decoded['message']?.toString() ?? response.body; } } on ApiException { message = response.body; } throw ApiException(message, statusCode: response.statusCode); } Map get _jsonHeaders => const { 'Content-Type': 'application/json', 'Accept': 'application/json', }; void dispose() { _client.close(); } static String _normalizeBaseUrl(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) { throw const ApiException('baseUrl 不能为空'); } final withScheme = trimmed.startsWith('http://') || trimmed.startsWith('https://') ? trimmed : 'http://$trimmed'; return withScheme.endsWith('/') ? withScheme.substring(0, withScheme.length - 1) : withScheme; } } class ApiException implements Exception { const ApiException(this.message, {this.statusCode}); final String message; final int? statusCode; @override String toString() => message; }