feat: Flutter 客户端 App + Web UI APK 下载入口
- 新增 Flutter 跨平台客户端项目 (clients/flutter/)
- 29 个 Dart 文件: 服务层/状态管理/5个页面/BLE配网
- BLE 蓝牙配网: 扫描设备、写入WiFi凭据、配网状态监听
- HTTP API 客户端: 覆盖全部端点 (播放/场景/WiFi/视频/配置/文件/插件)
- WebSocket 实时通信: 事件流 + 自动重连
- 暗色主题 Material 3 UI, 中文界面
- Android 配置: minSdkVersion 21, BLE/网络权限
- PRD 产品需求文档 + 开发任务看板
- Web UI 添加 APK 下载入口 (routes.rs)
- 下载弹窗 + 二维码 + /download/{filename} 静态文件路由
- BLE 插件增加自动重连循环 (ble/mod.rs)
- BLE 默认设备名修正为 'Showen' (config.rs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
196
clients/flutter/lib/providers/ble_provider.dart
Normal file
196
clients/flutter/lib/providers/ble_provider.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/ble_models.dart';
|
||||
import '../services/ble_service.dart';
|
||||
|
||||
class BleProvider extends ChangeNotifier {
|
||||
BleProvider({BleService? bleService}) : _bleService = bleService ?? BleService();
|
||||
|
||||
final BleService _bleService;
|
||||
|
||||
StreamSubscription<List<BleDevice>>? _scanSubscription;
|
||||
StreamSubscription<BleStatus>? _statusSubscription;
|
||||
|
||||
List<BleDevice> _devices = const <BleDevice>[];
|
||||
BleDevice? _selectedDevice;
|
||||
BleStatus? _latestStatus;
|
||||
ProvisioningState _provisioningState = ProvisioningState.scanning;
|
||||
String? _errorMessage;
|
||||
bool _isScanning = false;
|
||||
bool _isConnecting = false;
|
||||
bool _isProvisioning = false;
|
||||
bool _isConnected = false;
|
||||
bool _isDisposed = false;
|
||||
|
||||
List<BleDevice> get devices => _devices;
|
||||
BleDevice? get selectedDevice => _selectedDevice;
|
||||
BleStatus? get latestStatus => _latestStatus;
|
||||
ProvisioningState get provisioningState => _provisioningState;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isScanning => _isScanning;
|
||||
bool get isConnecting => _isConnecting;
|
||||
bool get isProvisioning => _isProvisioning;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<void> startScan() async {
|
||||
_errorMessage = null;
|
||||
_selectedDevice = null;
|
||||
_isConnected = false;
|
||||
_provisioningState = ProvisioningState.scanning;
|
||||
_isScanning = true;
|
||||
_notifySafely();
|
||||
|
||||
await _scanSubscription?.cancel();
|
||||
_scanSubscription = _bleService
|
||||
.scanForShowenDevices()
|
||||
.listen((List<BleDevice> scannedDevices) {
|
||||
_devices = scannedDevices;
|
||||
_notifySafely();
|
||||
}, onError: (Object error, StackTrace stackTrace) {
|
||||
_errorMessage = error.toString();
|
||||
_isScanning = false;
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
_notifySafely();
|
||||
});
|
||||
|
||||
Future<void>.delayed(const Duration(seconds: 6), () {
|
||||
if (_isScanning) {
|
||||
_isScanning = false;
|
||||
_notifySafely();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> connectToDevice(BleDevice device) async {
|
||||
_selectedDevice = device;
|
||||
_errorMessage = null;
|
||||
_isConnecting = true;
|
||||
_isScanning = false;
|
||||
_provisioningState = ProvisioningState.connecting;
|
||||
_notifySafely();
|
||||
|
||||
try {
|
||||
await _bleService.connectToDevice(device);
|
||||
await _subscribeToStatus();
|
||||
_isConnected = true;
|
||||
} catch (error) {
|
||||
_isConnected = false;
|
||||
_errorMessage = error.toString();
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
rethrow;
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
_notifySafely();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> provisionWifi(String ssid, String password) async {
|
||||
_errorMessage = null;
|
||||
_latestStatus = null;
|
||||
_isProvisioning = true;
|
||||
_provisioningState = ProvisioningState.writingCredentials;
|
||||
_notifySafely();
|
||||
|
||||
try {
|
||||
final Future<BleStatus> operation = _bleService.provisionWifi(
|
||||
ssid,
|
||||
password,
|
||||
timeout: const Duration(seconds: 30),
|
||||
);
|
||||
|
||||
Future<void>.delayed(const Duration(milliseconds: 400), () {
|
||||
if (_isProvisioning &&
|
||||
_provisioningState == ProvisioningState.writingCredentials) {
|
||||
_provisioningState = ProvisioningState.connectingWifi;
|
||||
_notifySafely();
|
||||
}
|
||||
});
|
||||
|
||||
final BleStatus result = await operation;
|
||||
_latestStatus = result;
|
||||
_provisioningState = result.ok
|
||||
? ProvisioningState.success
|
||||
: ProvisioningState.failed;
|
||||
if (!result.ok) {
|
||||
_errorMessage = result.error ?? 'WiFi provisioning failed';
|
||||
}
|
||||
} on TimeoutException {
|
||||
_errorMessage = 'BLE 配网超时(30 秒)';
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
rethrow;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
rethrow;
|
||||
} finally {
|
||||
_isProvisioning = false;
|
||||
_notifySafely();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
await _scanSubscription?.cancel();
|
||||
await _statusSubscription?.cancel();
|
||||
_scanSubscription = null;
|
||||
_statusSubscription = null;
|
||||
await _bleService.disconnect();
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_isProvisioning = false;
|
||||
_selectedDevice = null;
|
||||
_notifySafely();
|
||||
}
|
||||
|
||||
Future<void> retryScan() async {
|
||||
await disconnect();
|
||||
_devices = const <BleDevice>[];
|
||||
_latestStatus = null;
|
||||
_errorMessage = null;
|
||||
_provisioningState = ProvisioningState.scanning;
|
||||
_notifySafely();
|
||||
await startScan();
|
||||
}
|
||||
|
||||
Future<void> _subscribeToStatus() async {
|
||||
await _statusSubscription?.cancel();
|
||||
final Stream<BleStatus> stream = await _bleService.subscribeToStatus();
|
||||
_statusSubscription = stream.listen((BleStatus status) {
|
||||
_latestStatus = status;
|
||||
if (!status.ok) {
|
||||
_errorMessage = status.error ?? 'BLE status returned an error';
|
||||
}
|
||||
if (status.action == 'connect') {
|
||||
if (!status.ok) {
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
} else if (!status.isQueued) {
|
||||
_provisioningState = ProvisioningState.success;
|
||||
} else if (_isProvisioning) {
|
||||
_provisioningState = ProvisioningState.connectingWifi;
|
||||
}
|
||||
}
|
||||
_notifySafely();
|
||||
}, onError: (Object error, StackTrace stackTrace) {
|
||||
_errorMessage = error.toString();
|
||||
_provisioningState = ProvisioningState.failed;
|
||||
_notifySafely();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
unawaited(_scanSubscription?.cancel());
|
||||
unawaited(_statusSubscription?.cancel());
|
||||
unawaited(_bleService.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _notifySafely() {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
233
clients/flutter/lib/providers/device_provider.dart
Normal file
233
clients/flutter/lib/providers/device_provider.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/ble_status.dart';
|
||||
import '../models/device_status.dart';
|
||||
import '../models/player_status.dart';
|
||||
import '../models/wifi_status.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import '../services/web_socket_service.dart';
|
||||
|
||||
class DeviceProvider extends ChangeNotifier {
|
||||
DeviceProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required WebSocketService webSocketService,
|
||||
String initialDeviceIp = '127.0.0.1',
|
||||
}) : _httpApiService = httpApiService,
|
||||
_webSocketService = webSocketService,
|
||||
_deviceIp = _normalizeDeviceIp(initialDeviceIp) {
|
||||
_httpApiService.baseUrl = 'http://$_deviceIp:8080';
|
||||
_connectionSubscription = _webSocketService.onConnectionChanged.listen(
|
||||
_handleConnectionChanged,
|
||||
);
|
||||
_statusSubscription = _webSocketService.onStatusUpdate.listen(_handleStatusUpdate);
|
||||
_wifiSubscription = _webSocketService.onWifiUpdate.listen(_handleWifiUpdate);
|
||||
_bleSubscription = _webSocketService.onBleUpdate.listen(_handleBleUpdate);
|
||||
}
|
||||
|
||||
final HttpApiService _httpApiService;
|
||||
final WebSocketService _webSocketService;
|
||||
|
||||
late final StreamSubscription<SocketConnectionStatus> _connectionSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _statusSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _wifiSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _bleSubscription;
|
||||
|
||||
DeviceStatus _status = DeviceStatus.initial();
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool _webSocketConnected = false;
|
||||
String _deviceIp;
|
||||
|
||||
DeviceStatus get status => _status;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get webSocketConnected => _webSocketConnected;
|
||||
String get deviceIp => _deviceIp;
|
||||
HttpApiService get httpApiService => _httpApiService;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await refresh();
|
||||
await connect();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_httpApiService.getPlaybackStatus(),
|
||||
_httpApiService.getWifiStatus(),
|
||||
_httpApiService.getBleStatus(),
|
||||
]);
|
||||
|
||||
_status = _buildStatus(
|
||||
playerStatus: results[0] as PlayerStatus,
|
||||
wifiStatus: results[1] as WifiStatus,
|
||||
bleStatus: results[2] as BleServiceStatus,
|
||||
);
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
_status = _status.copyWith(
|
||||
connected: false,
|
||||
connectionType: 'offline',
|
||||
ipAddress: _deviceIp,
|
||||
);
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadDeviceOverview() => refresh();
|
||||
|
||||
Future<void> connect() async {
|
||||
try {
|
||||
await _webSocketService.connect(_deviceIp);
|
||||
_webSocketConnected = _webSocketService.isConnected;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_webSocketConnected = false;
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateDeviceIp(String ip) async {
|
||||
final normalized = _normalizeDeviceIp(ip);
|
||||
_deviceIp = normalized;
|
||||
_httpApiService.baseUrl = 'http://$_deviceIp:8080';
|
||||
_status = _status.copyWith(ipAddress: normalized, updatedAt: DateTime.now());
|
||||
notifyListeners();
|
||||
|
||||
await _webSocketService.disconnect();
|
||||
await initialize();
|
||||
}
|
||||
|
||||
Future<void> startBle({String? deviceName}) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.startBle(deviceName);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopBle() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.stopBle();
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleConnectionChanged(SocketConnectionStatus connectionStatus) {
|
||||
_webSocketConnected = connectionStatus == SocketConnectionStatus.connected;
|
||||
if (!_webSocketConnected) {
|
||||
_status = _status.copyWith(connectionType: _status.connectionType);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleStatusUpdate(Map<String, dynamic> payload) {
|
||||
final playerStatus = PlayerStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: playerStatus,
|
||||
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
|
||||
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleWifiUpdate(Map<String, dynamic> payload) {
|
||||
final wifiStatus = WifiStatus.fromJson(payload);
|
||||
_status = _buildStatus(
|
||||
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
|
||||
wifiStatus: wifiStatus,
|
||||
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleBleUpdate(Map<String, dynamic> payload) {
|
||||
final normalized = <String, dynamic>{
|
||||
'running': payload['running'] ?? payload['ready'] ?? false,
|
||||
'embedded': payload['embedded'] ?? false,
|
||||
'device_name': payload['device_name'] ?? payload['name'],
|
||||
};
|
||||
final bleStatus = BleServiceStatus.fromJson(normalized);
|
||||
_status = _buildStatus(
|
||||
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
|
||||
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
|
||||
bleStatus: bleStatus,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
DeviceStatus _buildStatus({
|
||||
required PlayerStatus playerStatus,
|
||||
required WifiStatus wifiStatus,
|
||||
required BleServiceStatus bleStatus,
|
||||
}) {
|
||||
final connectionType = wifiStatus.connected
|
||||
? 'wifi'
|
||||
: bleStatus.running
|
||||
? 'ble'
|
||||
: 'offline';
|
||||
|
||||
return DeviceStatus(
|
||||
connected: wifiStatus.connected || bleStatus.running || _webSocketConnected,
|
||||
connectionType: connectionType,
|
||||
deviceName: bleStatus.deviceName ?? 'ShowenV2',
|
||||
ipAddress: wifiStatus.ip ?? _deviceIp,
|
||||
playerStatus: playerStatus,
|
||||
wifiStatus: wifiStatus,
|
||||
bleStatus: bleStatus,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static String _normalizeDeviceIp(String input) {
|
||||
var value = input.trim();
|
||||
if (value.startsWith('http://')) {
|
||||
value = value.substring(7);
|
||||
} else if (value.startsWith('https://')) {
|
||||
value = value.substring(8);
|
||||
} else if (value.startsWith('ws://')) {
|
||||
value = value.substring(5);
|
||||
}
|
||||
final slashIndex = value.indexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
value = value.substring(0, slashIndex);
|
||||
}
|
||||
final colonIndex = value.indexOf(':');
|
||||
if (colonIndex >= 0) {
|
||||
value = value.substring(0, colonIndex);
|
||||
}
|
||||
return value.isEmpty ? '127.0.0.1' : value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_connectionSubscription.cancel());
|
||||
unawaited(_statusSubscription.cancel());
|
||||
unawaited(_wifiSubscription.cancel());
|
||||
unawaited(_bleSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
184
clients/flutter/lib/providers/player_provider.dart
Normal file
184
clients/flutter/lib/providers/player_provider.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/player_status.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import '../services/web_socket_service.dart';
|
||||
|
||||
class PlayerProvider extends ChangeNotifier {
|
||||
PlayerProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required WebSocketService webSocketService,
|
||||
}) : _httpApiService = httpApiService,
|
||||
_webSocketService = webSocketService {
|
||||
_statusSubscription = _webSocketService.onStatusUpdate.listen((payload) {
|
||||
_status = PlayerStatus.fromJson(payload);
|
||||
notifyListeners();
|
||||
});
|
||||
_stateSubscription = _webSocketService.onStateUpdate.listen((payload) {
|
||||
_currentState = payload['new_state']?.toString() ?? _currentState;
|
||||
notifyListeners();
|
||||
});
|
||||
_configSubscription = _webSocketService.onConfigUpdate.listen((payload) {
|
||||
_updateSceneOptions(payload);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
final HttpApiService _httpApiService;
|
||||
final WebSocketService _webSocketService;
|
||||
late final StreamSubscription<Map<String, dynamic>> _statusSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _stateSubscription;
|
||||
late final StreamSubscription<Map<String, dynamic>> _configSubscription;
|
||||
|
||||
PlayerStatus _status = PlayerStatus.initial();
|
||||
List<String> _playlist = const <String>[];
|
||||
List<String> _sceneOptions = const <String>['idle', 'intro', 'loop'];
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
String? _currentState;
|
||||
|
||||
PlayerStatus get status => _status;
|
||||
List<String> get playlist => _playlist;
|
||||
List<String> get sceneOptions => _sceneOptions;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String get currentState => _currentState ?? _status.currentVideo ?? 'idle';
|
||||
|
||||
Future<void> bootstrap() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_httpApiService.getPlaybackStatus(),
|
||||
_httpApiService.getPlaylist(),
|
||||
_httpApiService.getConfig(),
|
||||
]);
|
||||
_status = results[0] as PlayerStatus;
|
||||
_playlist = results[1] as List<String>;
|
||||
_updateSceneOptions(results[2] as Map<String, dynamic>);
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchStatus() async {
|
||||
try {
|
||||
_status = await _httpApiService.getPlaybackStatus();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchPlaylist() async {
|
||||
try {
|
||||
_playlist = await _httpApiService.getPlaylist();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> play() => _runCommand(_httpApiService.play);
|
||||
|
||||
Future<void> pause() => _runCommand(_httpApiService.pause);
|
||||
|
||||
Future<void> next() => _runCommand(_httpApiService.next);
|
||||
|
||||
Future<void> previous() => _runCommand(_httpApiService.previous);
|
||||
|
||||
Future<void> gotoIndex(int index) async {
|
||||
await _runCommand(() => _httpApiService.goto(index));
|
||||
}
|
||||
|
||||
Future<void> switchScene(String name) async {
|
||||
await _runCommand(() => _httpApiService.changeScene(name));
|
||||
_currentState = name;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> triggerEvent(String name, {String? value}) {
|
||||
return _runCommand(() => _httpApiService.trigger(name, value));
|
||||
}
|
||||
|
||||
Future<void> togglePlayPause() async {
|
||||
if (_status.running && !_status.paused) {
|
||||
await pause();
|
||||
return;
|
||||
}
|
||||
await play();
|
||||
}
|
||||
|
||||
Future<void> _runCommand(Future<dynamic> Function() action) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await action();
|
||||
await Future.wait<void>([
|
||||
fetchStatus(),
|
||||
fetchPlaylist(),
|
||||
]);
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSceneOptions(Map<String, dynamic> config) {
|
||||
final candidates = <String>{};
|
||||
final scenes = config['scenes'];
|
||||
if (scenes is List) {
|
||||
for (final scene in scenes) {
|
||||
final value = scene.toString();
|
||||
if (value.isNotEmpty) {
|
||||
candidates.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final stateMachine = config['state_machine'];
|
||||
if (stateMachine is Map) {
|
||||
final states = stateMachine['states'];
|
||||
if (states is Map) {
|
||||
for (final entry in states.keys) {
|
||||
final value = entry.toString();
|
||||
if (value.isNotEmpty) {
|
||||
candidates.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
final initialState = stateMachine['initial_state']?.toString();
|
||||
if (initialState != null && initialState.isNotEmpty) {
|
||||
candidates.add(initialState);
|
||||
_currentState ??= initialState;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isNotEmpty) {
|
||||
_sceneOptions = candidates.toList(growable: false)..sort();
|
||||
}
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_statusSubscription.cancel());
|
||||
unawaited(_stateSubscription.cancel());
|
||||
unawaited(_configSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
132
clients/flutter/lib/providers/wifi_provider.dart
Normal file
132
clients/flutter/lib/providers/wifi_provider.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/wifi_network.dart';
|
||||
import '../models/wifi_status.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import '../services/web_socket_service.dart';
|
||||
|
||||
class WifiProvider extends ChangeNotifier {
|
||||
WifiProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required WebSocketService webSocketService,
|
||||
}) : _httpApiService = httpApiService,
|
||||
_webSocketService = webSocketService {
|
||||
_wifiSubscription = _webSocketService.onWifiUpdate.listen((payload) {
|
||||
_status = WifiStatus.fromJson(payload);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
final HttpApiService _httpApiService;
|
||||
final WebSocketService _webSocketService;
|
||||
late final StreamSubscription<Map<String, dynamic>> _wifiSubscription;
|
||||
|
||||
WifiStatus _status = WifiStatus.disconnected();
|
||||
List<WifiNetwork> _networks = const <WifiNetwork>[];
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool _hotspotEnabled = false;
|
||||
|
||||
WifiStatus get status => _status;
|
||||
List<WifiNetwork> get networks => _networks;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get hotspotEnabled => _hotspotEnabled;
|
||||
|
||||
Future<void> bootstrap() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_httpApiService.getWifiStatus(),
|
||||
_httpApiService.scanWifi(),
|
||||
]);
|
||||
_status = results[0] as WifiStatus;
|
||||
_networks = results[1] as List<WifiNetwork>;
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshStatus() async {
|
||||
try {
|
||||
_status = await _httpApiService.getWifiStatus();
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> scanNetworks() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
_networks = await _httpApiService.scanWifi();
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect({required String ssid, required String password}) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.connectWifi(ssid, password);
|
||||
await refreshStatus();
|
||||
_hotspotEnabled = false;
|
||||
_errorMessage = null;
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startHotspot({String? ssid, String? password}) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.startAP(ssid, password);
|
||||
_hotspotEnabled = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopHotspot() async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
await _httpApiService.stopAP();
|
||||
_hotspotEnabled = false;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
_errorMessage = error.toString();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_wifiSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user