feat: GeViScope SDK integration with C# Bridge and Flutter app
- Add GeViScope Bridge (C# .NET 8.0) on port 7720 - Full SDK wrapper for camera control, PTZ, actions/events - 17 REST API endpoints for GeViScope server interaction - Support for MCS (Media Channel Simulator) with 16 test channels - Real-time action/event streaming via PLC callbacks - Add GeViServer Bridge (C# .NET 8.0) on port 7710 - Integration with GeViSoft orchestration layer - Input/output control and event management - Update Python API with new routers - /api/geviscope/* - Proxy to GeViScope Bridge - /api/geviserver/* - Proxy to GeViServer Bridge - /api/excel/* - Excel import functionality - Add Flutter app GeViScope integration - GeViScopeRemoteDataSource with 17 API methods - GeViScopeBloc for state management - GeViScopeScreen with PTZ controls - App drawer navigation to GeViScope - Add SDK documentation (extracted from PDFs) - GeViScope SDK docs (7 parts + action reference) - GeViSoft SDK docs (12 chunks) - Add .mcp.json for Claude Code MCP server config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,18 +81,24 @@ class SecureStorageManager {
|
||||
|
||||
Future<String?> getUsername() async {
|
||||
try {
|
||||
return await storage.read(key: 'username');
|
||||
final username = await storage.read(key: 'username');
|
||||
if (username != null) return username;
|
||||
} catch (e) {
|
||||
throw CacheException('Failed to read username');
|
||||
print('Warning: Failed to read username from secure storage, using memory');
|
||||
}
|
||||
// Fallback to memory storage (which now uses localStorage on web)
|
||||
return TokenManager().username;
|
||||
}
|
||||
|
||||
Future<String?> getUserRole() async {
|
||||
try {
|
||||
return await storage.read(key: 'user_role');
|
||||
final role = await storage.read(key: 'user_role');
|
||||
if (role != null) return role;
|
||||
} catch (e) {
|
||||
throw CacheException('Failed to read user role');
|
||||
print('Warning: Failed to read user role from secure storage, using memory');
|
||||
}
|
||||
// Fallback to memory storage (which now uses localStorage on web)
|
||||
return TokenManager().userRole;
|
||||
}
|
||||
|
||||
// Clear all data
|
||||
|
||||
@@ -25,7 +25,8 @@ abstract class ServerLocalDataSource {
|
||||
Future<void> markServerAsSynced(String id, String type);
|
||||
|
||||
/// Replace all servers (used after fetching from API)
|
||||
Future<void> replaceAllServers(List<ServerModel> servers);
|
||||
/// If force=true, discards all local changes and replaces with fresh data
|
||||
Future<void> replaceAllServers(List<ServerModel> servers, {bool force = false});
|
||||
|
||||
/// Clear all local data
|
||||
Future<void> clearAll();
|
||||
@@ -127,27 +128,38 @@ class ServerLocalDataSourceImpl implements ServerLocalDataSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> replaceAllServers(List<ServerModel> servers) async {
|
||||
Future<void> replaceAllServers(List<ServerModel> servers, {bool force = false}) async {
|
||||
final b = await box;
|
||||
|
||||
// Don't clear dirty servers - keep them for sync
|
||||
final dirtyServers = await getDirtyServers();
|
||||
final dirtyKeys = dirtyServers.map((s) => _getKey(s.id, s.serverType)).toSet();
|
||||
if (force) {
|
||||
// Force mode: discard all local changes and replace with fresh data
|
||||
await b.clear();
|
||||
|
||||
// Clear only non-dirty servers
|
||||
await b.clear();
|
||||
|
||||
// Re-add dirty servers
|
||||
for (final dirty in dirtyServers) {
|
||||
await b.put(_getKey(dirty.id, dirty.serverType), dirty);
|
||||
}
|
||||
|
||||
// Add all fetched servers (but don't overwrite dirty ones)
|
||||
for (final server in servers) {
|
||||
final key = _getKey(server.id, server.serverType);
|
||||
if (!dirtyKeys.contains(key)) {
|
||||
// Add all fetched servers
|
||||
for (final server in servers) {
|
||||
final key = _getKey(server.id, server.serverType);
|
||||
await b.put(key, ServerHiveModel.fromServerModel(server));
|
||||
}
|
||||
} else {
|
||||
// Normal mode: preserve dirty servers for sync
|
||||
final dirtyServers = await getDirtyServers();
|
||||
final dirtyKeys = dirtyServers.map((s) => _getKey(s.id, s.serverType)).toSet();
|
||||
|
||||
// Clear all servers
|
||||
await b.clear();
|
||||
|
||||
// Re-add dirty servers
|
||||
for (final dirty in dirtyServers) {
|
||||
await b.put(_getKey(dirty.id, dirty.serverType), dirty);
|
||||
}
|
||||
|
||||
// Add all fetched servers (but don't overwrite dirty ones)
|
||||
for (final server in servers) {
|
||||
final key = _getKey(server.id, server.serverType);
|
||||
if (!dirtyKeys.contains(key)) {
|
||||
await b.put(key, ServerHiveModel.fromServerModel(server));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/constants/api_constants.dart';
|
||||
|
||||
/// Remote data source for GeViScope operations
|
||||
///
|
||||
/// This data source provides methods to interact with GeViScope Camera Server
|
||||
/// through the FastAPI backend, which wraps the GeViScope SDK.
|
||||
/// GeViScope handles video recording, PTZ control, and media channel management.
|
||||
class GeViScopeRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
GeViScopeRemoteDataSource({required Dio dio}) : _dio = dio;
|
||||
|
||||
// ============================================================================
|
||||
// Connection Management
|
||||
// ============================================================================
|
||||
|
||||
/// Connect to GeViScope Camera Server
|
||||
///
|
||||
/// [address] - Server address (e.g., 'localhost' or IP address)
|
||||
/// [username] - Username for authentication (default: sysadmin)
|
||||
/// [password] - Password (default: masterkey)
|
||||
Future<Map<String, dynamic>> connect({
|
||||
required String address,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeConnect,
|
||||
data: {
|
||||
'address': address,
|
||||
'username': username,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Connection failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect from GeViScope
|
||||
Future<Map<String, dynamic>> disconnect() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeDisconnect,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Disconnection failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get connection status
|
||||
Future<Map<String, dynamic>> getStatus() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
ApiConstants.geviScopeStatus,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Status check failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Media Channels
|
||||
// ============================================================================
|
||||
|
||||
/// Get list of media channels (cameras)
|
||||
Future<Map<String, dynamic>> getChannels() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
ApiConstants.geviScopeChannels,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Get channels failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh media channel list
|
||||
Future<Map<String, dynamic>> refreshChannels() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeChannelsRefresh,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Refresh channels failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Actions
|
||||
// ============================================================================
|
||||
|
||||
/// Send generic action to GeViScope
|
||||
///
|
||||
/// [action] - Action string (e.g., "CustomAction(1,\"Hello\")")
|
||||
Future<Map<String, dynamic>> sendAction(String action) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeAction,
|
||||
data: {
|
||||
'action': action,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Send action failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Send custom action
|
||||
///
|
||||
/// [typeId] - Action type ID
|
||||
/// [text] - Action text/parameters
|
||||
Future<Map<String, dynamic>> sendCustomAction({
|
||||
required int typeId,
|
||||
String text = '',
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCustomAction,
|
||||
queryParameters: {
|
||||
'type_id': typeId,
|
||||
'text': text,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('CustomAction failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Video Control
|
||||
// ============================================================================
|
||||
|
||||
/// CrossSwitch - Route video input to output
|
||||
///
|
||||
/// [videoInput] - Source camera/channel number
|
||||
/// [videoOutput] - Destination monitor/output number
|
||||
/// [switchMode] - Switch mode (0 = normal)
|
||||
Future<Map<String, dynamic>> crossSwitch({
|
||||
required int videoInput,
|
||||
required int videoOutput,
|
||||
int switchMode = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCrossSwitch,
|
||||
queryParameters: {
|
||||
'video_input': videoInput,
|
||||
'video_output': videoOutput,
|
||||
'switch_mode': switchMode,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('CrossSwitch failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PTZ Camera Control
|
||||
// ============================================================================
|
||||
|
||||
/// Pan camera left or right
|
||||
///
|
||||
/// [camera] - Camera/PTZ head number
|
||||
/// [direction] - 'left' or 'right'
|
||||
/// [speed] - Movement speed (1-100)
|
||||
Future<Map<String, dynamic>> cameraPan({
|
||||
required int camera,
|
||||
required String direction,
|
||||
int speed = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCameraPan,
|
||||
queryParameters: {
|
||||
'camera': camera,
|
||||
'direction': direction,
|
||||
'speed': speed,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Camera pan failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Tilt camera up or down
|
||||
///
|
||||
/// [camera] - Camera/PTZ head number
|
||||
/// [direction] - 'up' or 'down'
|
||||
/// [speed] - Movement speed (1-100)
|
||||
Future<Map<String, dynamic>> cameraTilt({
|
||||
required int camera,
|
||||
required String direction,
|
||||
int speed = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCameraTilt,
|
||||
queryParameters: {
|
||||
'camera': camera,
|
||||
'direction': direction,
|
||||
'speed': speed,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Camera tilt failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Zoom camera in or out
|
||||
///
|
||||
/// [camera] - Camera/PTZ head number
|
||||
/// [direction] - 'in' or 'out'
|
||||
/// [speed] - Movement speed (1-100)
|
||||
Future<Map<String, dynamic>> cameraZoom({
|
||||
required int camera,
|
||||
required String direction,
|
||||
int speed = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCameraZoom,
|
||||
queryParameters: {
|
||||
'camera': camera,
|
||||
'direction': direction,
|
||||
'speed': speed,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Camera zoom failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop all camera movement
|
||||
///
|
||||
/// [camera] - Camera/PTZ head number
|
||||
Future<Map<String, dynamic>> cameraStop({
|
||||
required int camera,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCameraStop,
|
||||
queryParameters: {
|
||||
'camera': camera,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Camera stop failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to camera preset position
|
||||
///
|
||||
/// [camera] - Camera/PTZ head number
|
||||
/// [preset] - Preset position number
|
||||
Future<Map<String, dynamic>> cameraPreset({
|
||||
required int camera,
|
||||
required int preset,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeCameraPreset,
|
||||
queryParameters: {
|
||||
'camera': camera,
|
||||
'preset': preset,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Camera preset failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Digital I/O
|
||||
// ============================================================================
|
||||
|
||||
/// Close digital output contact (activate relay)
|
||||
///
|
||||
/// [contactId] - Digital contact ID
|
||||
Future<Map<String, dynamic>> digitalIoClose({
|
||||
required int contactId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeDigitalIoClose,
|
||||
queryParameters: {
|
||||
'contact_id': contactId,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Digital I/O close failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Open digital output contact (deactivate relay)
|
||||
///
|
||||
/// [contactId] - Digital contact ID
|
||||
Future<Map<String, dynamic>> digitalIoOpen({
|
||||
required int contactId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeDigitalIoOpen,
|
||||
queryParameters: {
|
||||
'contact_id': contactId,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Digital I/O open failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Message Log
|
||||
// ============================================================================
|
||||
|
||||
/// Get received message log
|
||||
Future<Map<String, dynamic>> getMessages() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
ApiConstants.geviScopeMessages,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Get messages failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear message log
|
||||
Future<Map<String, dynamic>> clearMessages() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviScopeMessagesClear,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Clear messages failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/constants/api_constants.dart';
|
||||
|
||||
/// Remote data source for GeViServer operations
|
||||
///
|
||||
/// This data source provides methods to interact with GeViServer through
|
||||
/// the FastAPI backend, which wraps GeViProcAPI.dll
|
||||
class GeViServerRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
GeViServerRemoteDataSource({required Dio dio}) : _dio = dio;
|
||||
|
||||
// ============================================================================
|
||||
// Connection Management
|
||||
// ============================================================================
|
||||
|
||||
/// Connect to GeViServer
|
||||
///
|
||||
/// [address] - Server address (e.g., 'localhost' or IP address)
|
||||
/// [username] - Username for authentication
|
||||
/// [password] - Password (will be encrypted by backend)
|
||||
Future<Map<String, dynamic>> connect({
|
||||
required String address,
|
||||
required String username,
|
||||
required String password,
|
||||
String? username2,
|
||||
String? password2,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerConnect,
|
||||
data: {
|
||||
'address': address,
|
||||
'username': username,
|
||||
'password': password,
|
||||
if (username2 != null) 'username2': username2,
|
||||
if (password2 != null) 'password2': password2,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Connection failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect from GeViServer
|
||||
Future<Map<String, dynamic>> disconnect() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerDisconnect,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Disconnection failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get connection status
|
||||
Future<Map<String, dynamic>> getStatus() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
ApiConstants.geviServerStatus,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Status check failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Send ping to GeViServer
|
||||
Future<Map<String, dynamic>> sendPing() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerPing,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Ping failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Message Sending
|
||||
// ============================================================================
|
||||
|
||||
/// Send generic action message to GeViServer
|
||||
///
|
||||
/// [message] - ASCII action message (e.g., "CrossSwitch(7,3,0)")
|
||||
Future<Map<String, dynamic>> sendMessage(String message) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerSendMessage,
|
||||
data: {
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Send message failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Video Control
|
||||
// ============================================================================
|
||||
|
||||
/// Cross-switch video input to output
|
||||
///
|
||||
/// [videoInput] - Video input channel number
|
||||
/// [videoOutput] - Video output channel number
|
||||
/// [switchMode] - Switch mode (default: 0 for normal)
|
||||
Future<Map<String, dynamic>> crossSwitch({
|
||||
required int videoInput,
|
||||
required int videoOutput,
|
||||
int switchMode = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerVideoCrossSwitch,
|
||||
queryParameters: {
|
||||
'video_input': videoInput,
|
||||
'video_output': videoOutput,
|
||||
'switch_mode': switchMode,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('CrossSwitch failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear video output
|
||||
///
|
||||
/// [videoOutput] - Video output channel number
|
||||
Future<Map<String, dynamic>> clearOutput({
|
||||
required int videoOutput,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerVideoClearOutput,
|
||||
queryParameters: {
|
||||
'video_output': videoOutput,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('ClearOutput failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Digital I/O Control
|
||||
// ============================================================================
|
||||
|
||||
/// Close digital output contact
|
||||
///
|
||||
/// [contactId] - Digital contact ID
|
||||
Future<Map<String, dynamic>> closeContact({
|
||||
required int contactId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerDigitalIoCloseContact,
|
||||
queryParameters: {
|
||||
'contact_id': contactId,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('CloseContact failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Open digital output contact
|
||||
///
|
||||
/// [contactId] - Digital contact ID
|
||||
Future<Map<String, dynamic>> openContact({
|
||||
required int contactId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerDigitalIoOpenContact,
|
||||
queryParameters: {
|
||||
'contact_id': contactId,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('OpenContact failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timer Control
|
||||
// ============================================================================
|
||||
|
||||
/// Start timer
|
||||
///
|
||||
/// [timerId] - Timer ID (0 to use name)
|
||||
/// [timerName] - Timer name
|
||||
Future<Map<String, dynamic>> startTimer({
|
||||
int timerId = 0,
|
||||
String timerName = '',
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerTimerStart,
|
||||
queryParameters: {
|
||||
'timer_id': timerId,
|
||||
'timer_name': timerName,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('StartTimer failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop timer
|
||||
///
|
||||
/// [timerId] - Timer ID (0 to use name)
|
||||
/// [timerName] - Timer name
|
||||
Future<Map<String, dynamic>> stopTimer({
|
||||
int timerId = 0,
|
||||
String timerName = '',
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerTimerStop,
|
||||
queryParameters: {
|
||||
'timer_id': timerId,
|
||||
'timer_name': timerName,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('StopTimer failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Custom Actions
|
||||
// ============================================================================
|
||||
|
||||
/// Send custom action
|
||||
///
|
||||
/// [typeId] - Action type ID
|
||||
/// [text] - Action text/parameters
|
||||
Future<Map<String, dynamic>> sendCustomAction({
|
||||
required int typeId,
|
||||
String text = '',
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.geviServerCustomAction,
|
||||
queryParameters: {
|
||||
'type_id': typeId,
|
||||
'text': text,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('CustomAction failed: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
161
geutebruck_app/lib/data/services/excel_import_service.dart
Normal file
161
geutebruck_app/lib/data/services/excel_import_service.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../domain/entities/server.dart';
|
||||
import '../../core/constants/api_constants.dart';
|
||||
import '../../core/storage/token_manager.dart';
|
||||
import '../data_sources/local/server_local_data_source.dart';
|
||||
import '../models/server_hive_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ExcelImportService {
|
||||
final _uuid = const Uuid();
|
||||
final Dio _dio = Dio();
|
||||
final ServerLocalDataSource? _localDataSource;
|
||||
|
||||
ExcelImportService({ServerLocalDataSource? localDataSource})
|
||||
: _localDataSource = localDataSource;
|
||||
|
||||
/// Import servers from Excel file using backend API
|
||||
/// Expected columns (starting from row 2):
|
||||
/// - Column B: Hostname/Alias
|
||||
/// - Column C: Type (GeViScope or G-Core)
|
||||
/// - Column D: IP Server/Host
|
||||
/// - Column E: Username
|
||||
/// - Column F: Password
|
||||
Future<List<Server>> importServersFromExcel(Uint8List fileBytes, String fileName) async {
|
||||
try {
|
||||
print('[ExcelImport] Starting import, file size: ${fileBytes.length} bytes');
|
||||
|
||||
// Get auth token
|
||||
final token = TokenManager().accessToken;
|
||||
|
||||
// Prepare multipart request
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(
|
||||
fileBytes,
|
||||
filename: fileName,
|
||||
),
|
||||
});
|
||||
|
||||
// Call backend API
|
||||
final response = await _dio.post(
|
||||
'${ApiConstants.baseUrl}/excel/import-servers',
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server returned status ${response.statusCode}');
|
||||
}
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final serversData = data['servers'] as List<dynamic>;
|
||||
|
||||
print('[ExcelImport] Server returned ${serversData.length} servers');
|
||||
|
||||
// Convert API response to Server entities
|
||||
final servers = <Server>[];
|
||||
for (final serverData in serversData) {
|
||||
final serverType = serverData['type'] == 'gcore'
|
||||
? ServerType.gcore
|
||||
: ServerType.geviscope;
|
||||
|
||||
final server = Server(
|
||||
id: _uuid.v4(),
|
||||
alias: serverData['alias'] as String,
|
||||
host: serverData['host'] as String,
|
||||
user: serverData['user'] as String? ?? 'sysadmin',
|
||||
password: serverData['password'] as String? ?? '',
|
||||
type: serverType,
|
||||
enabled: serverData['enabled'] as bool? ?? true,
|
||||
deactivateEcho: serverData['deactivateEcho'] as bool? ?? false,
|
||||
deactivateLiveCheck: serverData['deactivateLiveCheck'] as bool? ?? false,
|
||||
);
|
||||
|
||||
servers.add(server);
|
||||
}
|
||||
|
||||
print('[ExcelImport] Import completed: ${servers.length} servers parsed');
|
||||
return servers;
|
||||
} catch (e) {
|
||||
print('[ExcelImport] Fatal error: $e');
|
||||
if (e is DioException) {
|
||||
print('[ExcelImport] DioException type: ${e.type}');
|
||||
print('[ExcelImport] Response status: ${e.response?.statusCode}');
|
||||
print('[ExcelImport] Response data: ${e.response?.data}');
|
||||
print('[ExcelImport] Request URL: ${e.requestOptions.uri}');
|
||||
|
||||
final errorMessage = e.response?.data?['detail'] ??
|
||||
e.response?.data?['error'] ??
|
||||
e.message ??
|
||||
'Unknown error';
|
||||
throw Exception('Failed to import Excel file: $errorMessage');
|
||||
}
|
||||
throw Exception('Failed to import Excel file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge imported servers with existing servers
|
||||
/// Only adds servers that don't already exist (based on alias or host)
|
||||
List<Server> mergeServers({
|
||||
required List<Server> existing,
|
||||
required List<Server> imported,
|
||||
}) {
|
||||
final newServers = <Server>[];
|
||||
int duplicateCount = 0;
|
||||
|
||||
for (final importedServer in imported) {
|
||||
// Check if server already exists by alias or host
|
||||
final isDuplicate = existing.any((existingServer) =>
|
||||
existingServer.alias.toLowerCase() == importedServer.alias.toLowerCase() ||
|
||||
existingServer.host.toLowerCase() == importedServer.host.toLowerCase());
|
||||
|
||||
if (!isDuplicate) {
|
||||
newServers.add(importedServer);
|
||||
print('[ExcelImport] New server: ${importedServer.alias}');
|
||||
} else {
|
||||
duplicateCount++;
|
||||
print('[ExcelImport] Duplicate skipped: ${importedServer.alias}');
|
||||
}
|
||||
}
|
||||
|
||||
print('[ExcelImport] Merge complete: ${newServers.length} new servers, $duplicateCount duplicates skipped');
|
||||
return newServers;
|
||||
}
|
||||
|
||||
/// Save imported servers directly to local storage as dirty (unsaved) servers
|
||||
/// This bypasses the bloc to avoid triggering multiple rebuilds during import
|
||||
Future<void> saveImportedServersToStorage(List<Server> servers) async {
|
||||
if (_localDataSource == null) {
|
||||
throw Exception('LocalDataSource not available for direct storage access');
|
||||
}
|
||||
|
||||
print('[ExcelImport] Saving ${servers.length} servers directly to storage...');
|
||||
|
||||
for (final server in servers) {
|
||||
final hiveModel = ServerHiveModel(
|
||||
id: server.id,
|
||||
alias: server.alias,
|
||||
host: server.host,
|
||||
user: server.user,
|
||||
password: server.password,
|
||||
serverType: server.type == ServerType.gcore ? 'gcore' : 'geviscope',
|
||||
enabled: server.enabled,
|
||||
deactivateEcho: server.deactivateEcho,
|
||||
deactivateLiveCheck: server.deactivateLiveCheck,
|
||||
isDirty: true, // Mark as dirty (unsaved change)
|
||||
syncOperation: 'create', // Needs to be created on server
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
|
||||
await _localDataSource!.saveServer(hiveModel);
|
||||
print('[ExcelImport] Saved to storage: ${server.alias}');
|
||||
}
|
||||
|
||||
print('[ExcelImport] All ${servers.length} servers saved to storage as unsaved changes');
|
||||
}
|
||||
}
|
||||
@@ -139,8 +139,8 @@ class SyncServiceImpl implements SyncService {
|
||||
// Fetch all servers from API
|
||||
final servers = await remoteDataSource.getAllServers();
|
||||
|
||||
// Replace local storage (preserving dirty servers)
|
||||
await localDataSource.replaceAllServers(servers);
|
||||
// Replace local storage with force=true to discard all local changes
|
||||
await localDataSource.replaceAllServers(servers, force: true);
|
||||
|
||||
return Right(servers.length);
|
||||
} on ServerException catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user