Compare commits
1 Commits
v1.0.0-MVP
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e83e4277 |
@@ -1,5 +1,5 @@
|
|||||||
# Start Geutebruck API Services
|
# Start Geutebruck API Services
|
||||||
# This script starts GeViServer, SDK Bridge, and Python API
|
# This script starts GeViServer, SDK Bridge, Python API, and Flutter Web App
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ $sdkBridgeExe = "$sdkBridgePath\GeViScopeBridge.exe"
|
|||||||
$apiPath = "C:\DEV\COPILOT\geutebruck-api\src\api"
|
$apiPath = "C:\DEV\COPILOT\geutebruck-api\src\api"
|
||||||
$venvPython = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\python.exe"
|
$venvPython = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\python.exe"
|
||||||
$uvicorn = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\uvicorn.exe"
|
$uvicorn = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\uvicorn.exe"
|
||||||
|
$flutterWebPath = "C:\DEV\COPILOT\geutebruck_app\build\web"
|
||||||
|
|
||||||
# Function to wait for port to be listening
|
# Function to wait for port to be listening
|
||||||
function Wait-ForPort {
|
function Wait-ForPort {
|
||||||
@@ -40,12 +41,13 @@ function Wait-ForPort {
|
|||||||
$geviServerRunning = Get-Process -Name "GeViServer" -ErrorAction SilentlyContinue
|
$geviServerRunning = Get-Process -Name "GeViServer" -ErrorAction SilentlyContinue
|
||||||
$sdkBridgeRunning = Get-Process -Name "GeViScopeBridge" -ErrorAction SilentlyContinue
|
$sdkBridgeRunning = Get-Process -Name "GeViScopeBridge" -ErrorAction SilentlyContinue
|
||||||
$uvicornRunning = Get-Process -Name "uvicorn" -ErrorAction SilentlyContinue
|
$uvicornRunning = Get-Process -Name "uvicorn" -ErrorAction SilentlyContinue
|
||||||
|
$flutterRunning = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Start GeViServer
|
# Start GeViServer
|
||||||
if ($geviServerRunning) {
|
if ($geviServerRunning) {
|
||||||
Write-Host "[SKIP] GeViServer is already running (PID: $($geviServerRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] GeViServer is already running (PID: $($geviServerRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[1/3] Starting GeViServer..." -ForegroundColor Green
|
Write-Host "[1/4] Starting GeViServer..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $geviServerExe -ArgumentList "console" -WorkingDirectory "C:\GEVISOFT" -WindowStyle Hidden
|
Start-Process -FilePath $geviServerExe -ArgumentList "console" -WorkingDirectory "C:\GEVISOFT" -WindowStyle Hidden
|
||||||
|
|
||||||
# Wait for GeViServer to start listening on port 7700
|
# Wait for GeViServer to start listening on port 7700
|
||||||
@@ -65,7 +67,7 @@ if ($geviServerRunning) {
|
|||||||
if ($sdkBridgeRunning) {
|
if ($sdkBridgeRunning) {
|
||||||
Write-Host "[SKIP] SDK Bridge is already running (PID: $($sdkBridgeRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] SDK Bridge is already running (PID: $($sdkBridgeRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[2/3] Starting SDK Bridge..." -ForegroundColor Green
|
Write-Host "[2/4] Starting SDK Bridge..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $sdkBridgeExe -WorkingDirectory $sdkBridgePath -WindowStyle Hidden
|
Start-Process -FilePath $sdkBridgeExe -WorkingDirectory $sdkBridgePath -WindowStyle Hidden
|
||||||
|
|
||||||
# Wait for SDK Bridge to start listening on port 50051
|
# Wait for SDK Bridge to start listening on port 50051
|
||||||
@@ -85,7 +87,7 @@ if ($sdkBridgeRunning) {
|
|||||||
if ($uvicornRunning) {
|
if ($uvicornRunning) {
|
||||||
Write-Host "[SKIP] Python API is already running (PID: $($uvicornRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] Python API is already running (PID: $($uvicornRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] Starting Python API..." -ForegroundColor Green
|
Write-Host "[3/4] Starting Python API..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $uvicorn `
|
Start-Process -FilePath $uvicorn `
|
||||||
-ArgumentList "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" `
|
-ArgumentList "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" `
|
||||||
-WorkingDirectory $apiPath `
|
-WorkingDirectory $apiPath `
|
||||||
@@ -104,6 +106,32 @@ if ($uvicornRunning) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Start Flutter Web Server
|
||||||
|
if ($flutterRunning) {
|
||||||
|
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host "[SKIP] Flutter Web is already running (PID: $flutterProcess)" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "[4/4] Starting Flutter Web Server..." -ForegroundColor Green
|
||||||
|
Start-Process -FilePath "python" `
|
||||||
|
-ArgumentList "-m", "http.server", "8081", "--bind", "0.0.0.0" `
|
||||||
|
-WorkingDirectory $flutterWebPath `
|
||||||
|
-WindowStyle Hidden
|
||||||
|
|
||||||
|
# Wait for Flutter Web to start listening on port 8081
|
||||||
|
Write-Host " Waiting for Flutter Web to initialize" -NoNewline -ForegroundColor Gray
|
||||||
|
if (Wait-ForPort -Port 8081 -TimeoutSeconds 10) {
|
||||||
|
Write-Host ""
|
||||||
|
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host " [OK] Flutter Web started (PID: $flutterProcess)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [ERROR] Flutter Web failed to start listening on port 8081" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
Write-Host "Services Started Successfully!" -ForegroundColor Green
|
Write-Host "Services Started Successfully!" -ForegroundColor Green
|
||||||
@@ -113,6 +141,7 @@ Write-Host "GeViServer: Running on ports 7700-7703" -ForegroundColor Cyan
|
|||||||
Write-Host "SDK Bridge: Running on port 50051 (gRPC)" -ForegroundColor Cyan
|
Write-Host "SDK Bridge: Running on port 50051 (gRPC)" -ForegroundColor Cyan
|
||||||
Write-Host "Python API: http://localhost:8000" -ForegroundColor Cyan
|
Write-Host "Python API: http://localhost:8000" -ForegroundColor Cyan
|
||||||
Write-Host "Swagger UI: http://localhost:8000/docs" -ForegroundColor Cyan
|
Write-Host "Swagger UI: http://localhost:8000/docs" -ForegroundColor Cyan
|
||||||
|
Write-Host "Flutter Web: http://localhost:8081" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "To check status, run: .\status-services.ps1" -ForegroundColor Yellow
|
Write-Host "To check status, run: .\status-services.ps1" -ForegroundColor Yellow
|
||||||
Write-Host "To stop services, run: .\stop-services.ps1" -ForegroundColor Yellow
|
Write-Host "To stop services, run: .\stop-services.ps1" -ForegroundColor Yellow
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stop Geutebruck API Services
|
# Stop Geutebruck API Services
|
||||||
# This script stops Python API, SDK Bridge, and GeViServer
|
# This script stops Flutter Web, Python API, SDK Bridge, and GeViServer
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# .\stop-services.ps1 # Stop all services
|
# .\stop-services.ps1 # Stop all services
|
||||||
@@ -20,42 +20,53 @@ if ($KeepGeViServer) {
|
|||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
# Stop Flutter Web Server
|
||||||
|
$flutterPort = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||||
|
if ($flutterPort) {
|
||||||
|
$flutterPid = $flutterPort | Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host "[1/4] Stopping Flutter Web (PID: $flutterPid)..." -ForegroundColor Yellow
|
||||||
|
Stop-Process -Id $flutterPid -Force
|
||||||
|
Write-Host " [OK] Flutter Web stopped" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "[1/4] Flutter Web is not running" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
# Stop Python API
|
# Stop Python API
|
||||||
$uvicorn = Get-Process -Name "uvicorn"
|
$uvicorn = Get-Process -Name "uvicorn"
|
||||||
if ($uvicorn) {
|
if ($uvicorn) {
|
||||||
Write-Host "[1/3] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
Write-Host "[2/4] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "uvicorn" -Force
|
Stop-Process -Name "uvicorn" -Force
|
||||||
Write-Host " [OK] Python API stopped" -ForegroundColor Green
|
Write-Host " [OK] Python API stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[1/3] Python API is not running" -ForegroundColor Gray
|
Write-Host "[2/4] Python API is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop SDK Bridge
|
# Stop SDK Bridge
|
||||||
$sdkBridge = Get-Process -Name "GeViScopeBridge"
|
$sdkBridge = Get-Process -Name "GeViScopeBridge"
|
||||||
if ($sdkBridge) {
|
if ($sdkBridge) {
|
||||||
Write-Host "[2/3] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
Write-Host "[3/4] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "GeViScopeBridge" -Force
|
Stop-Process -Name "GeViScopeBridge" -Force
|
||||||
Write-Host " [OK] SDK Bridge stopped" -ForegroundColor Green
|
Write-Host " [OK] SDK Bridge stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[2/3] SDK Bridge is not running" -ForegroundColor Gray
|
Write-Host "[3/4] SDK Bridge is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop GeViServer (unless -KeepGeViServer flag is set)
|
# Stop GeViServer (unless -KeepGeViServer flag is set)
|
||||||
if (-not $KeepGeViServer) {
|
if (-not $KeepGeViServer) {
|
||||||
$geviServer = Get-Process -Name "GeViServer"
|
$geviServer = Get-Process -Name "GeViServer"
|
||||||
if ($geviServer) {
|
if ($geviServer) {
|
||||||
Write-Host "[3/3] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
Write-Host "[4/4] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "GeViServer" -Force
|
Stop-Process -Name "GeViServer" -Force
|
||||||
Write-Host " [OK] GeViServer stopped" -ForegroundColor Green
|
Write-Host " [OK] GeViServer stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] GeViServer is not running" -ForegroundColor Gray
|
Write-Host "[4/4] GeViServer is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$geviServer = Get-Process -Name "GeViServer"
|
$geviServer = Get-Process -Name "GeViServer"
|
||||||
if ($geviServer) {
|
if ($geviServer) {
|
||||||
Write-Host "[3/3] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
Write-Host "[4/4] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
Write-Host "[4/4] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../models/action_template.dart';
|
||||||
|
|
||||||
|
/// Service for caching server lists to prevent categories from disappearing
|
||||||
|
/// when SDK Bridge is temporarily unavailable during service restarts
|
||||||
|
class ServerCacheService {
|
||||||
|
static const String _gcoreServersKey = 'cached_gcore_servers';
|
||||||
|
static const String _gscServersKey = 'cached_gsc_servers';
|
||||||
|
static const String _cacheTimestampKey = 'server_cache_timestamp';
|
||||||
|
|
||||||
|
/// Save G-Core servers to cache
|
||||||
|
static Future<void> cacheGCoreServers(List<ServerInfo> servers) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||||
|
await prefs.setString(_gcoreServersKey, json);
|
||||||
|
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
print('[ServerCache] Cached ${servers.length} G-Core servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save GSC servers to cache
|
||||||
|
static Future<void> cacheGSCServers(List<ServerInfo> servers) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||||
|
await prefs.setString(_gscServersKey, json);
|
||||||
|
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
print('[ServerCache] Cached ${servers.length} GSC servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save both server lists
|
||||||
|
static Future<void> cacheServers({
|
||||||
|
required List<ServerInfo> gcoreServers,
|
||||||
|
required List<ServerInfo> gscServers,
|
||||||
|
}) async {
|
||||||
|
await cacheGCoreServers(gcoreServers);
|
||||||
|
await cacheGSCServers(gscServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load cached G-Core servers
|
||||||
|
static Future<List<ServerInfo>> getCachedGCoreServers() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = prefs.getString(_gcoreServersKey);
|
||||||
|
if (json == null) {
|
||||||
|
print('[ServerCache] No cached G-Core servers found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(json) as List<dynamic>;
|
||||||
|
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||||
|
print('[ServerCache] Loaded ${servers.length} cached G-Core servers');
|
||||||
|
return servers;
|
||||||
|
} catch (e) {
|
||||||
|
print('[ServerCache] Error loading cached G-Core servers: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load cached GSC servers
|
||||||
|
static Future<List<ServerInfo>> getCachedGSCServers() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = prefs.getString(_gscServersKey);
|
||||||
|
if (json == null) {
|
||||||
|
print('[ServerCache] No cached GSC servers found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(json) as List<dynamic>;
|
||||||
|
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||||
|
print('[ServerCache] Loaded ${servers.length} cached GSC servers');
|
||||||
|
return servers;
|
||||||
|
} catch (e) {
|
||||||
|
print('[ServerCache] Error loading cached GSC servers: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cache age in milliseconds
|
||||||
|
static Future<int?> getCacheAge() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final timestamp = prefs.getInt(_cacheTimestampKey);
|
||||||
|
if (timestamp == null) return null;
|
||||||
|
return DateTime.now().millisecondsSinceEpoch - timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all cached server data
|
||||||
|
static Future<void> clearCache() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_gcoreServersKey);
|
||||||
|
await prefs.remove(_gscServersKey);
|
||||||
|
await prefs.remove(_cacheTimestampKey);
|
||||||
|
print('[ServerCache] Cache cleared');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import '../../../domain/entities/action_mapping.dart';
|
|||||||
import '../../../data/models/action_output.dart';
|
import '../../../data/models/action_output.dart';
|
||||||
import '../../../data/models/action_template.dart';
|
import '../../../data/models/action_template.dart';
|
||||||
import '../../../data/services/action_template_service.dart';
|
import '../../../data/services/action_template_service.dart';
|
||||||
|
import '../../../data/services/server_cache_service.dart';
|
||||||
import '../../../core/constants/api_constants.dart';
|
import '../../../core/constants/api_constants.dart';
|
||||||
import '../../../core/storage/token_manager.dart';
|
import '../../../core/storage/token_manager.dart';
|
||||||
import '../../blocs/action_mapping/action_mapping_bloc.dart';
|
import '../../blocs/action_mapping/action_mapping_bloc.dart';
|
||||||
@@ -73,6 +74,13 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadActionTemplates() async {
|
Future<void> _loadActionTemplates() async {
|
||||||
|
print('[ActionTemplates] Starting to load action templates...');
|
||||||
|
|
||||||
|
// Load cached servers first as fallback
|
||||||
|
final cachedGCoreServers = await ServerCacheService.getCachedGCoreServers();
|
||||||
|
final cachedGSCServers = await ServerCacheService.getCachedGSCServers();
|
||||||
|
print('[ActionTemplates] Cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get auth token from TokenManager
|
// Get auth token from TokenManager
|
||||||
final token = TokenManager().accessToken;
|
final token = TokenManager().accessToken;
|
||||||
@@ -85,22 +93,58 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
|||||||
final categoriesResponse = await service.getActionCategories();
|
final categoriesResponse = await service.getActionCategories();
|
||||||
final templates = await service.getActionTemplates();
|
final templates = await service.getActionTemplates();
|
||||||
|
|
||||||
|
// Use API data if available, otherwise use cached data
|
||||||
|
final gcoreServers = categoriesResponse.servers.gcoreServers.isNotEmpty
|
||||||
|
? categoriesResponse.servers.gcoreServers
|
||||||
|
: cachedGCoreServers;
|
||||||
|
|
||||||
|
final gscServers = categoriesResponse.servers.gscServers.isNotEmpty
|
||||||
|
? categoriesResponse.servers.gscServers
|
||||||
|
: cachedGSCServers;
|
||||||
|
|
||||||
|
print('[ActionTemplates] API returned - G-Core: ${categoriesResponse.servers.gcoreServers.length}, GSC: ${categoriesResponse.servers.gscServers.length}');
|
||||||
|
print('[ActionTemplates] Using - G-Core: ${gcoreServers.length}, GSC: ${gscServers.length}');
|
||||||
|
|
||||||
|
// Cache the server lists if we got them from API
|
||||||
|
if (categoriesResponse.servers.gcoreServers.isNotEmpty || categoriesResponse.servers.gscServers.isNotEmpty) {
|
||||||
|
await ServerCacheService.cacheServers(
|
||||||
|
gcoreServers: categoriesResponse.servers.gcoreServers,
|
||||||
|
gscServers: categoriesResponse.servers.gscServers,
|
||||||
|
);
|
||||||
|
print('[ActionTemplates] Cached server lists to local storage');
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_categories = categoriesResponse.categories;
|
_categories = categoriesResponse.categories;
|
||||||
_templates = templates;
|
_templates = templates;
|
||||||
_gcoreServers = categoriesResponse.servers.gcoreServers;
|
_gcoreServers = gcoreServers;
|
||||||
_gscServers = categoriesResponse.servers.gscServers;
|
_gscServers = gscServers;
|
||||||
_isLoadingTemplates = false;
|
_isLoadingTemplates = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
print('[ActionTemplates] Successfully loaded templates and ${gcoreServers.length + gscServers.length} servers');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('[ActionTemplates] Error loading from API: $e');
|
||||||
|
|
||||||
|
// Fall back to cached data
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_gcoreServers = cachedGCoreServers;
|
||||||
|
_gscServers = cachedGSCServers;
|
||||||
_isLoadingTemplates = false;
|
_isLoadingTemplates = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
print('[ActionTemplates] Fell back to cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final hasCache = cachedGCoreServers.isNotEmpty || cachedGSCServers.isNotEmpty;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Failed to load action templates: $e'),
|
content: Text(
|
||||||
backgroundColor: Colors.orange,
|
hasCache
|
||||||
|
? 'Using cached server data (API unavailable)'
|
||||||
|
: 'Failed to load action templates: $e',
|
||||||
|
),
|
||||||
|
backgroundColor: hasCache ? Colors.orange : Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,17 @@ class ActionMappingsListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||||
bool _showSearch = false;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final Set<String> _selectedMappings = {};
|
||||||
|
bool _selectAll = false;
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
bool? _filterEnabled; // null = all, true = enabled only, false = disabled only
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
// Sort states
|
||||||
|
int? _sortColumnIndex;
|
||||||
|
bool _sortAscending = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -27,51 +36,113 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ActionMapping> _getFilteredMappings(List<ActionMapping> allMappings) {
|
||||||
|
var filtered = allMappings;
|
||||||
|
|
||||||
|
// Apply enabled/disabled filter
|
||||||
|
if (_filterEnabled != null) {
|
||||||
|
filtered = filtered.where((m) => m.enabled == _filterEnabled).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (_searchQuery.isNotEmpty) {
|
||||||
|
final query = _searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.where((m) {
|
||||||
|
return m.name.toLowerCase().contains(query) ||
|
||||||
|
m.inputAction.toLowerCase().contains(query) ||
|
||||||
|
m.outputActions.any((o) => o.action.toLowerCase().contains(query)) ||
|
||||||
|
(m.description?.toLowerCase().contains(query) ?? false);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (_sortColumnIndex != null) {
|
||||||
|
filtered.sort((a, b) {
|
||||||
|
int comparison = 0;
|
||||||
|
switch (_sortColumnIndex) {
|
||||||
|
case 0: // Name
|
||||||
|
comparison = a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 1: // Input Action
|
||||||
|
comparison = a.inputAction.toLowerCase().compareTo(b.inputAction.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 2: // Output Actions
|
||||||
|
final aOutputs = a.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||||
|
final bOutputs = b.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||||
|
comparison = aOutputs.compareTo(bOutputs);
|
||||||
|
break;
|
||||||
|
case 3: // Status
|
||||||
|
comparison = a.enabled == b.enabled ? 0 : (a.enabled ? -1 : 1);
|
||||||
|
break;
|
||||||
|
case 4: // Executions
|
||||||
|
comparison = a.executionCount.compareTo(b.executionCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return _sortAscending ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleSelectAll(List<ActionMapping> mappings) {
|
||||||
|
setState(() {
|
||||||
|
if (_selectAll) {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
} else {
|
||||||
|
_selectedMappings.addAll(mappings.map((m) => m.id));
|
||||||
|
}
|
||||||
|
_selectAll = !_selectAll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBatchDeleteDialog(BuildContext context) {
|
||||||
|
if (_selectedMappings.isEmpty) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text('Delete ${_selectedMappings.length} Action Mappings'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to delete ${_selectedMappings.length} selected action mapping${_selectedMappings.length != 1 ? 's' : ''}?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
for (final id in _selectedMappings) {
|
||||||
|
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(id));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Delete All', style: TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(currentRoute: '/action-mappings'),
|
drawer: const AppDrawer(currentRoute: '/action-mappings'),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _showSearch
|
title: Row(
|
||||||
? TextField(
|
children: [
|
||||||
controller: _searchController,
|
const Icon(Icons.link, size: 24),
|
||||||
autofocus: true,
|
const SizedBox(width: 8),
|
||||||
decoration: const InputDecoration(
|
const Text('Action Mappings'),
|
||||||
hintText: 'Search action mappings...',
|
],
|
||||||
border: InputBorder.none,
|
),
|
||||||
hintStyle: TextStyle(color: Colors.white70),
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
onChanged: (query) {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
|
||||||
} else {
|
|
||||||
context.read<ActionMappingBloc>().add(SearchActionMappings(query));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.link, size: 24),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text('Action Mappings'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
// Search toggle button
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(_showSearch ? Icons.close : Icons.search),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_showSearch = !_showSearch;
|
|
||||||
if (!_showSearch) {
|
|
||||||
_searchController.clear();
|
|
||||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: _showSearch ? 'Close search' : 'Search',
|
|
||||||
),
|
|
||||||
// Sync button with dirty count badge
|
// Sync button with dirty count badge
|
||||||
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -158,24 +229,136 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Add Action Mapping button
|
// Toolbar with search, filters, and batch actions
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
color: Colors.grey[100],
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
// Search and Add button row
|
||||||
onPressed: () {
|
Row(
|
||||||
context.push('/action-mappings/create');
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search by name, input, output, or description...',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_searchController.clear();
|
||||||
|
_searchQuery = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
context.push('/action-mappings/create');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add Mapping'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Filter chips and batch actions
|
||||||
|
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final allMappings = state is ActionMappingLoaded ? state.mappings : <ActionMapping>[];
|
||||||
|
final filteredMappings = _getFilteredMappings(allMappings);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Filter chips
|
||||||
|
const Text('Filters: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('All'),
|
||||||
|
selected: _filterEnabled == null,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Enabled'),
|
||||||
|
selected: _filterEnabled == true,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? true : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Disabled'),
|
||||||
|
selected: _filterEnabled == false,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? false : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Batch actions
|
||||||
|
if (_selectedMappings.isNotEmpty) ...[
|
||||||
|
Text(
|
||||||
|
'${_selectedMappings.length} selected',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _showBatchDeleteDialog(context),
|
||||||
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
|
label: const Text('Delete Selected'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Add Action Mapping'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Action Mapping list
|
// Table
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocConsumer<ActionMappingBloc, ActionMappingState>(
|
child: BlocConsumer<ActionMappingBloc, ActionMappingState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
@@ -231,9 +414,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (state is ActionMappingLoaded) {
|
} else if (state is ActionMappingLoaded) {
|
||||||
final mappings = state.mappings;
|
final filteredMappings = _getFilteredMappings(state.mappings);
|
||||||
|
|
||||||
if (mappings.isEmpty) {
|
if (filteredMappings.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -241,14 +424,18 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
Icon(Icons.link_off, size: 64, color: Colors.grey[400]),
|
Icon(Icons.link_off, size: 64, color: Colors.grey[400]),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No action mappings found',
|
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||||
|
? 'No matching action mappings'
|
||||||
|
: 'No action mappings found',
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Add an action mapping to get started',
|
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Add an action mapping to get started',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
),
|
),
|
||||||
@@ -258,14 +445,7 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return _buildCompactTable(context, filteredMappings);
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
itemCount: mappings.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final mapping = mappings[index];
|
|
||||||
return _buildActionMappingCard(context, mapping);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (state is ActionMappingError) {
|
} else if (state is ActionMappingError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -298,7 +478,6 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ActionMappingInitial or any other unknown states with a loading indicator
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -308,241 +487,193 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionMappingCard(BuildContext context, ActionMapping mapping) {
|
Widget _buildCompactTable(BuildContext context, List<ActionMapping> mappings) {
|
||||||
final hasParameters = mapping.inputParameters.isNotEmpty ||
|
return SingleChildScrollView(
|
||||||
mapping.outputActions.any((o) => o.parameters.isNotEmpty);
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
return Card(
|
child: DataTable(
|
||||||
margin: const EdgeInsets.only(bottom: 12.0),
|
columnSpacing: 24,
|
||||||
child: ExpansionTile(
|
horizontalMargin: 12,
|
||||||
leading: CircleAvatar(
|
headingRowHeight: 48,
|
||||||
backgroundColor: mapping.enabled
|
dataRowHeight: 56,
|
||||||
? Colors.green.withOpacity(0.2)
|
showCheckboxColumn: true,
|
||||||
: Colors.grey.withOpacity(0.2),
|
sortColumnIndex: _sortColumnIndex,
|
||||||
child: Icon(
|
sortAscending: _sortAscending,
|
||||||
Icons.link,
|
columns: [
|
||||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
DataColumn(
|
||||||
),
|
label: const Text('Name', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
onSort: (columnIndex, ascending) {
|
||||||
title: Row(
|
setState(() {
|
||||||
children: [
|
_sortColumnIndex = columnIndex;
|
||||||
Expanded(
|
_sortAscending = ascending;
|
||||||
child: Text(
|
});
|
||||||
mapping.name,
|
},
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
DataColumn(
|
||||||
Container(
|
label: const Text('Input Action', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
onSort: (columnIndex, ascending) {
|
||||||
decoration: BoxDecoration(
|
setState(() {
|
||||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
_sortColumnIndex = columnIndex;
|
||||||
borderRadius: BorderRadius.circular(12),
|
_sortAscending = ascending;
|
||||||
),
|
});
|
||||||
child: Text(
|
},
|
||||||
mapping.enabled ? 'Enabled' : 'Disabled',
|
),
|
||||||
style: const TextStyle(
|
DataColumn(
|
||||||
color: Colors.white,
|
label: const Text('Output Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
fontSize: 12,
|
onSort: (columnIndex, ascending) {
|
||||||
),
|
setState(() {
|
||||||
),
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Executions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
numeric: true,
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const DataColumn(
|
||||||
|
label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
rows: mappings.map((mapping) {
|
||||||
subtitle: Column(
|
final isSelected = _selectedMappings.contains(mapping.id);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
return DataRow(
|
||||||
const SizedBox(height: 4),
|
selected: isSelected,
|
||||||
if (mapping.description != null && mapping.description!.isNotEmpty)
|
onSelectChanged: (selected) {
|
||||||
Text(
|
setState(() {
|
||||||
mapping.description!,
|
if (selected == true) {
|
||||||
maxLines: 2,
|
_selectedMappings.add(mapping.id);
|
||||||
overflow: TextOverflow.ellipsis,
|
} else {
|
||||||
),
|
_selectedMappings.remove(mapping.id);
|
||||||
const SizedBox(height: 4),
|
}
|
||||||
Row(
|
_selectAll = _selectedMappings.length == mappings.length;
|
||||||
children: [
|
});
|
||||||
const Text('Input: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
},
|
||||||
Expanded(
|
cells: [
|
||||||
child: Text(
|
// Name
|
||||||
|
DataCell(
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
mapping.name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (mapping.description != null && mapping.description!.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
mapping.description!,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Input Action
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
mapping.inputAction,
|
mapping.inputAction,
|
||||||
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (mapping.inputParameters.isNotEmpty)
|
|
||||||
|
// Output Actions
|
||||||
|
DataCell(
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
mapping.outputActions.map((o) => o.action).join(', '),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
DataCell(
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(left: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withOpacity(0.2),
|
color: mapping.enabled ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
border: Border.all(
|
||||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
color: mapping.enabled ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${mapping.inputParameters.length} param${mapping.inputParameters.length != 1 ? 's' : ''}',
|
mapping.enabled ? 'Enabled' : 'Disabled',
|
||||||
style: TextStyle(fontSize: 10, color: Colors.blue[700]),
|
style: TextStyle(
|
||||||
|
color: mapping.enabled ? Colors.green[700] : Colors.grey[700],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
...mapping.outputActions.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final output = entry.value;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
index == 0 ? 'Output: ' : ' ',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
output.action,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (output.parameters.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(left: 4),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${output.parameters.length} param${output.parameters.length != 1 ? 's' : ''}',
|
|
||||||
style: TextStyle(fontSize: 10, color: Colors.orange[700]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}),
|
|
||||||
if (mapping.executionCount > 0) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text('Executions: ${mapping.executionCount}'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
if (hasParameters)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Input Parameters
|
|
||||||
if (mapping.inputParameters.isNotEmpty) ...[
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Input Parameters',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...mapping.inputParameters.entries.map((entry) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Text(
|
|
||||||
'${entry.key}:',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
entry.value.toString(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// Output Action Parameters
|
|
||||||
...mapping.outputActions.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final output = entry.value;
|
|
||||||
if (output.parameters.isEmpty) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return Column(
|
// Executions
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
DataCell(
|
||||||
children: [
|
Text(
|
||||||
const Divider(),
|
'${mapping.executionCount}',
|
||||||
const SizedBox(height: 8),
|
style: const TextStyle(fontSize: 12),
|
||||||
Text(
|
),
|
||||||
'Output Action ${index + 1}: ${output.action}',
|
),
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
// Actions
|
||||||
color: Colors.orange[700],
|
DataCell(
|
||||||
),
|
Row(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 8),
|
children: [
|
||||||
...output.parameters.entries.map((paramEntry) {
|
IconButton(
|
||||||
return Padding(
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
onPressed: () {
|
||||||
child: Row(
|
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
},
|
||||||
children: [
|
tooltip: 'Edit',
|
||||||
SizedBox(
|
padding: EdgeInsets.zero,
|
||||||
width: 120,
|
constraints: const BoxConstraints(),
|
||||||
child: Text(
|
),
|
||||||
'${paramEntry.key}:',
|
const SizedBox(width: 8),
|
||||||
style: const TextStyle(
|
IconButton(
|
||||||
fontWeight: FontWeight.w500,
|
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||||
fontSize: 12,
|
onPressed: () {
|
||||||
),
|
_showDeleteConfirmation(context, mapping);
|
||||||
),
|
},
|
||||||
),
|
tooltip: 'Delete',
|
||||||
Expanded(
|
padding: EdgeInsets.zero,
|
||||||
child: Text(
|
constraints: const BoxConstraints(),
|
||||||
paramEntry.value.toString(),
|
),
|
||||||
style: const TextStyle(fontSize: 12),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onPressed: () {
|
|
||||||
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
|
||||||
},
|
|
||||||
tooltip: 'Edit',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
|
||||||
onPressed: () {
|
|
||||||
_showDeleteConfirmation(context, mapping);
|
|
||||||
},
|
|
||||||
tooltip: 'Delete',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -562,6 +693,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(mapping.id));
|
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(mapping.id));
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.remove(mapping.id);
|
||||||
|
});
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,10 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
_selectedActionName = actionName;
|
_selectedActionName = actionName;
|
||||||
_selectedTemplate = widget.templates[actionName];
|
_selectedTemplate = widget.templates[actionName];
|
||||||
|
|
||||||
|
print('[DEBUG] _selectAction: action=$actionName, categoryPrefix=$_categoryPrefix');
|
||||||
|
print('[DEBUG] _selectAction: gscServers count=${widget.gscServers.length}');
|
||||||
|
print('[DEBUG] _selectAction: gcoreServers count=${widget.gcoreServers.length}');
|
||||||
|
|
||||||
// Clear previous parameter controllers
|
// Clear previous parameter controllers
|
||||||
for (final controller in _paramControllers.values) {
|
for (final controller in _paramControllers.values) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
@@ -121,6 +125,7 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
|
|
||||||
// Initialize new parameter controllers
|
// Initialize new parameter controllers
|
||||||
if (_selectedTemplate != null) {
|
if (_selectedTemplate != null) {
|
||||||
|
print('[DEBUG] _selectAction: template parameters=${_selectedTemplate!.parameters}');
|
||||||
for (final param in _selectedTemplate!.parameters) {
|
for (final param in _selectedTemplate!.parameters) {
|
||||||
_paramControllers[param] = TextEditingController();
|
_paramControllers[param] = TextEditingController();
|
||||||
_paramEnabled[param] = false; // Start with parameters disabled
|
_paramEnabled[param] = false; // Start with parameters disabled
|
||||||
@@ -130,29 +135,41 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
if (_categoryPrefix == 'gcore') {
|
if (_categoryPrefix == 'gcore') {
|
||||||
// Add G-Core server parameter
|
// Add G-Core server parameter
|
||||||
const serverParam = 'GCoreServer';
|
const serverParam = 'GCoreServer';
|
||||||
|
print('[DEBUG] Adding G-Core server parameter');
|
||||||
if (!_paramControllers.containsKey(serverParam)) {
|
if (!_paramControllers.containsKey(serverParam)) {
|
||||||
_paramControllers[serverParam] = TextEditingController();
|
_paramControllers[serverParam] = TextEditingController();
|
||||||
|
print('[DEBUG] Created new controller for $serverParam');
|
||||||
}
|
}
|
||||||
_paramEnabled[serverParam] = true;
|
_paramEnabled[serverParam] = true;
|
||||||
// Auto-select first enabled server if available
|
// Auto-select first enabled server if available
|
||||||
final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList();
|
final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList();
|
||||||
if (enabledServers.isNotEmpty) {
|
if (enabledServers.isNotEmpty) {
|
||||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||||
|
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||||
}
|
}
|
||||||
} else if (_categoryPrefix == 'gsc') {
|
} else if (_categoryPrefix == 'gsc') {
|
||||||
// Add GSC server parameter
|
// Add GSC server parameter
|
||||||
const serverParam = 'GscServer';
|
const serverParam = 'GscServer';
|
||||||
|
print('[DEBUG] Adding GSC server parameter');
|
||||||
if (!_paramControllers.containsKey(serverParam)) {
|
if (!_paramControllers.containsKey(serverParam)) {
|
||||||
_paramControllers[serverParam] = TextEditingController();
|
_paramControllers[serverParam] = TextEditingController();
|
||||||
|
print('[DEBUG] Created new controller for $serverParam');
|
||||||
}
|
}
|
||||||
_paramEnabled[serverParam] = true;
|
_paramEnabled[serverParam] = true;
|
||||||
// Auto-select first enabled server if available
|
// Auto-select first enabled server if available
|
||||||
final enabledServers = widget.gscServers.where((s) => s.enabled).toList();
|
final enabledServers = widget.gscServers.where((s) => s.enabled).toList();
|
||||||
|
print('[DEBUG] GSC enabled servers: ${enabledServers.map((s) => s.alias).toList()}');
|
||||||
if (enabledServers.isNotEmpty) {
|
if (enabledServers.isNotEmpty) {
|
||||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||||
|
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||||
|
} else {
|
||||||
|
print('[DEBUG] WARNING: No enabled GSC servers found!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] Final _paramEnabled keys: ${_paramEnabled.keys.toList()}');
|
||||||
|
print('[DEBUG] Final _paramControllers keys: ${_paramControllers.keys.toList()}');
|
||||||
|
|
||||||
// Auto-fill caption if empty
|
// Auto-fill caption if empty
|
||||||
if (_captionController.text.isEmpty) {
|
if (_captionController.text.isEmpty) {
|
||||||
_captionController.text = actionName;
|
_captionController.text = actionName;
|
||||||
@@ -197,13 +214,47 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate server parameter for GSC/G-Core actions
|
||||||
|
if (_categoryPrefix == 'gsc') {
|
||||||
|
final gscServerValue = _paramControllers['GscServer']?.text.trim() ?? '';
|
||||||
|
if (gscServerValue.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('GSC server is required. Please configure a GeViScope server first.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (_categoryPrefix == 'gcore') {
|
||||||
|
final gcoreServerValue = _paramControllers['GCoreServer']?.text.trim() ?? '';
|
||||||
|
if (gcoreServerValue.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('G-Core server is required. Please configure a G-Core server first.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Building parameters...');
|
||||||
|
print('[DEBUG] _onOk: _categoryPrefix=$_categoryPrefix');
|
||||||
|
print('[DEBUG] _onOk: _paramEnabled=${_paramEnabled}');
|
||||||
|
|
||||||
// Build parameters map from enabled parameters
|
// Build parameters map from enabled parameters
|
||||||
final parameters = <String, dynamic>{};
|
final parameters = <String, dynamic>{};
|
||||||
for (final entry in _paramEnabled.entries) {
|
for (final entry in _paramEnabled.entries) {
|
||||||
|
print('[DEBUG] _onOk: Checking param ${entry.key}, enabled=${entry.value}');
|
||||||
if (entry.value) {
|
if (entry.value) {
|
||||||
final value = _paramControllers[entry.key]?.text.trim() ?? '';
|
final value = _paramControllers[entry.key]?.text.trim() ?? '';
|
||||||
|
print('[DEBUG] _onOk: Param ${entry.key} value="$value"');
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
parameters[entry.key] = value;
|
parameters[entry.key] = value;
|
||||||
|
print('[DEBUG] _onOk: Added param ${entry.key}=$value');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,12 +271,16 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
parameters['Delay'] = delay;
|
parameters['Delay'] = delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Final parameters=$parameters');
|
||||||
|
|
||||||
// Create ActionOutput with the actual action name (NOT the caption!)
|
// Create ActionOutput with the actual action name (NOT the caption!)
|
||||||
final result = ActionOutput(
|
final result = ActionOutput(
|
||||||
action: _selectedActionName!,
|
action: _selectedActionName!,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Created ActionOutput: action=$_selectedActionName, parameters=$parameters');
|
||||||
|
|
||||||
Navigator.of(context).pop(result);
|
Navigator.of(context).pop(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,15 +292,19 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
enhanced.addAll(widget.categories);
|
enhanced.addAll(widget.categories);
|
||||||
|
|
||||||
// Add G-Core variants for applicable categories
|
// Add G-Core variants for applicable categories
|
||||||
|
// ALWAYS show these categories - even if no servers are configured
|
||||||
|
// User can see the category but won't be able to select a server if none exist
|
||||||
for (final category in _serverCategories) {
|
for (final category in _serverCategories) {
|
||||||
if (widget.categories.containsKey(category) && widget.gcoreServers.isNotEmpty) {
|
if (widget.categories.containsKey(category)) {
|
||||||
enhanced['G-Core: $category'] = widget.categories[category]!;
|
enhanced['G-Core: $category'] = widget.categories[category]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add GSC variants for applicable categories
|
// Add GSC variants for applicable categories
|
||||||
|
// ALWAYS show these categories - even if no servers are configured
|
||||||
|
// User can see the category but won't be able to select a server if none exist
|
||||||
for (final category in _serverCategories) {
|
for (final category in _serverCategories) {
|
||||||
if (widget.categories.containsKey(category) && widget.gscServers.isNotEmpty) {
|
if (widget.categories.containsKey(category)) {
|
||||||
enhanced['GSC: $category'] = widget.categories[category]!;
|
enhanced['GSC: $category'] = widget.categories[category]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,12 +317,15 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
if (displayCategory.startsWith('G-Core: ')) {
|
if (displayCategory.startsWith('G-Core: ')) {
|
||||||
_categoryPrefix = 'gcore';
|
_categoryPrefix = 'gcore';
|
||||||
_selectedCategory = displayCategory.substring(8); // Remove "G-Core: "
|
_selectedCategory = displayCategory.substring(8); // Remove "G-Core: "
|
||||||
|
print('[DEBUG] Parsed category: prefix=gcore, base=$_selectedCategory');
|
||||||
} else if (displayCategory.startsWith('GSC: ')) {
|
} else if (displayCategory.startsWith('GSC: ')) {
|
||||||
_categoryPrefix = 'gsc';
|
_categoryPrefix = 'gsc';
|
||||||
_selectedCategory = displayCategory.substring(5); // Remove "GSC: "
|
_selectedCategory = displayCategory.substring(5); // Remove "GSC: "
|
||||||
|
print('[DEBUG] Parsed category: prefix=gsc, base=$_selectedCategory');
|
||||||
} else {
|
} else {
|
||||||
_categoryPrefix = null;
|
_categoryPrefix = null;
|
||||||
_selectedCategory = displayCategory;
|
_selectedCategory = displayCategory;
|
||||||
|
print('[DEBUG] Parsed category: prefix=null, base=$_selectedCategory');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user