| import okhttp3.OkHttpClient | import okhttp3.OkHttpClient | ||||
| import androidx.lifecycle.ViewModel | import androidx.lifecycle.ViewModel | ||||
| import androidx.lifecycle.ViewModelProvider | import androidx.lifecycle.ViewModelProvider | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.runtime.collectAsState | import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | import androidx.compose.runtime.getValue | ||||
| import androidx.activity.result.contract.ActivityResultContracts | import androidx.activity.result.contract.ActivityResultContracts | ||||
| import android.Manifest | import android.Manifest | ||||
| import java.util.concurrent.TimeUnit | |||||
| class MainActivity : ComponentActivity() { | class MainActivity : ComponentActivity() { | ||||
| private val client = OkHttpClient() | |||||
| private val client = OkHttpClient.Builder() | |||||
| .pingInterval(10, TimeUnit.SECONDS) | |||||
| .connectTimeout(10, TimeUnit.SECONDS) | |||||
| .readTimeout(0, TimeUnit.SECONDS) | |||||
| .build() | |||||
| private val webSocketManager = LedWebSocketManager(client) | private val webSocketManager = LedWebSocketManager(client) | ||||
| private val repository = LedRepository(webSocketManager) | private val repository = LedRepository(webSocketManager) | ||||
| private lateinit var settingsRepository: SettingsRepository | private lateinit var settingsRepository: SettingsRepository | ||||
| settingsRepository = SettingsRepository(applicationContext) | settingsRepository = SettingsRepository(applicationContext) | ||||
| discoveryManager = LedDiscoveryManager(applicationContext) | discoveryManager = LedDiscoveryManager(applicationContext) | ||||
| // In a real app, use Hilt or Koin. Manual DI for simplicity. | |||||
| val viewModelFactory = object : ViewModelProvider.Factory { | val viewModelFactory = object : ViewModelProvider.Factory { | ||||
| override fun <T : ViewModel> create(modelClass: Class<T>): T { | override fun <T : ViewModel> create(modelClass: Class<T>): T { | ||||
| @Suppress("UNCHECKED_CAST") | |||||
| return LedViewModel(repository, settingsRepository, discoveryManager) as T | return LedViewModel(repository, settingsRepository, discoveryManager) as T | ||||
| } | } | ||||
| } | } |
| encodeDefaults = true | encodeDefaults = true | ||||
| } | } | ||||
| fun connect(url: String) { | |||||
| if (currentUrl == url && (_connectionState.value is ConnectionState.Connecting || _connectionState.value is ConnectionState.Connected)) { | |||||
| fun connect(url: String, force: Boolean = false) { | |||||
| if (!force && currentUrl == url && (_connectionState.value is ConnectionState.Connecting || _connectionState.value is ConnectionState.Connected)) { | |||||
| return | return | ||||
| } | } | ||||
| // If switching URLs, disconnect existing first | |||||
| if (currentUrl != url) { | |||||
| disconnect() | |||||
| } | |||||
| // Ensure a clean state before connecting | |||||
| disconnect() | |||||
| currentUrl = url | currentUrl = url | ||||
| val request = Request.Builder().url(url).build() | val request = Request.Builder().url(url).build() |
| val ledState: StateFlow<LedState> = webSocketManager.ledState | val ledState: StateFlow<LedState> = webSocketManager.ledState | ||||
| val ledConfig: StateFlow<LedConfig> = webSocketManager.ledConfig | val ledConfig: StateFlow<LedConfig> = webSocketManager.ledConfig | ||||
| fun connect(url: String) { | |||||
| webSocketManager.connect(url) | |||||
| fun connect(url: String, force: Boolean = false) { | |||||
| webSocketManager.connect(url, force) | |||||
| } | } | ||||
| fun disconnect() { | fun disconnect() { |
| Spacer(modifier = Modifier.weight(1f)) | Spacer(modifier = Modifier.weight(1f)) | ||||
| NavigationDrawerItem( | NavigationDrawerItem( | ||||
| label = { Text("Manage Devices") }, | label = { Text("Manage Devices") }, | ||||
| selected = selectedTab == 2, | |||||
| selected = selectedTab == 3, | |||||
| onClick = { | onClick = { | ||||
| selectedTab = 2 | |||||
| selectedTab = 3 | |||||
| scope.launch { drawerState.close() } | scope.launch { drawerState.close() } | ||||
| }, | }, | ||||
| icon = { Icon(Icons.Default.Settings, contentDescription = null) }, | icon = { Icon(Icons.Default.Settings, contentDescription = null) }, | ||||
| NavigationBarItem( | NavigationBarItem( | ||||
| selected = selectedTab == 1, | selected = selectedTab == 1, | ||||
| onClick = { selectedTab = 1 }, | onClick = { selectedTab = 1 }, | ||||
| icon = { Icon(Icons.Default.GridView, contentDescription = "Style") }, | |||||
| label = { Text("Style") } | |||||
| icon = { Icon(Icons.Default.GridView, contentDescription = "Modes") }, | |||||
| label = { Text("Modes") } | |||||
| ) | ) | ||||
| NavigationBarItem( | NavigationBarItem( | ||||
| selected = selectedTab == 2, | selected = selectedTab == 2, | ||||
| NavigationBarItem( | NavigationBarItem( | ||||
| selected = selectedTab == 4, | selected = selectedTab == 4, | ||||
| onClick = { selectedTab = 4 }, | onClick = { selectedTab = 4 }, | ||||
| icon = { Icon(Icons.Default.Palette, contentDescription = "Appearance") }, | |||||
| label = { Text("Appearance") } | |||||
| icon = { Icon(Icons.Default.Palette, contentDescription = "Style") }, | |||||
| label = { Text("Style") } | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| .verticalScroll(rememberScrollState()), | .verticalScroll(rememberScrollState()), | ||||
| horizontalAlignment = Alignment.CenterHorizontally | horizontalAlignment = Alignment.CenterHorizontally | ||||
| ) { | ) { | ||||
| Text("Animation Styles", style = MaterialTheme.typography.headlineMedium) | |||||
| Text("Animated Modes", style = MaterialTheme.typography.headlineMedium) | |||||
| Spacer(modifier = Modifier.height(24.dp)) | Spacer(modifier = Modifier.height(24.dp)) | ||||
| LedModeType.entries.forEach { mode -> | LedModeType.entries.forEach { mode -> | ||||
| .verticalScroll(rememberScrollState()), | .verticalScroll(rememberScrollState()), | ||||
| horizontalAlignment = Alignment.CenterHorizontally | horizontalAlignment = Alignment.CenterHorizontally | ||||
| ) { | ) { | ||||
| Text("Appearance", style = MaterialTheme.typography.headlineMedium) | |||||
| Text("Style", style = MaterialTheme.typography.headlineMedium) | |||||
| Spacer(modifier = Modifier.height(24.dp)) | Spacer(modifier = Modifier.height(24.dp)) | ||||
| Text("Theme Mode", style = MaterialTheme.typography.titleMedium) | Text("Theme Mode", style = MaterialTheme.typography.titleMedium) |
| fun selectDevice(deviceId: String?) { | fun selectDevice(deviceId: String?) { | ||||
| viewModelScope.launch { | viewModelScope.launch { | ||||
| if (deviceId != null && deviceId == selectedDeviceId.value) { | |||||
| if (deviceId != null) { | |||||
| devices.value.find { it.id == deviceId }?.let { device -> | devices.value.find { it.id == deviceId }?.let { device -> | ||||
| repository.connect("ws://${device.ip}:${device.port}") | |||||
| // Always try to connect if it's a manual selection, even if currently "connected" | |||||
| repository.connect("ws://${device.ip}:${device.port}", force = true) | |||||
| } | } | ||||
| } | } | ||||
| settingsRepository.selectDevice(deviceId) | settingsRepository.selectDevice(deviceId) |