Skip to content

Commit 939a860

Browse files
committed
feat: add command buttons
- Commands includes lock, hibernate, logout, restart and shutdown. On long click, you can set a timer for when to execute them. - Som subtle Ui changes and refactoring
1 parent d7932d9 commit 939a860

File tree

11 files changed

+265
-45
lines changed

11 files changed

+265
-45
lines changed

app/src/main/java/com/castle/sefirah/presentation/home/HomeScreen.kt

+61-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
package com.castle.sefirah.presentation.home
22

3+
import android.graphics.drawable.Icon
34
import androidx.compose.foundation.layout.Arrangement
45
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
57
import androidx.compose.foundation.layout.fillMaxSize
68
import androidx.compose.foundation.layout.fillMaxWidth
79
import androidx.compose.foundation.layout.padding
810
import androidx.compose.foundation.lazy.LazyColumn
11+
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.filled.Lock
13+
import androidx.compose.material.icons.filled.PowerSettingsNew
14+
import androidx.compose.material.icons.filled.RestartAlt
15+
import androidx.compose.material.icons.filled.Schedule
916
import androidx.compose.material3.BottomSheetScaffold
1017
import androidx.compose.material3.ExperimentalMaterial3Api
18+
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.IconButton
1120
import androidx.compose.material3.SheetState
1221
import androidx.compose.material3.SheetValue
1322
import androidx.compose.material3.rememberBottomSheetScaffoldState
1423
import androidx.compose.runtime.Composable
1524
import androidx.compose.runtime.collectAsState
1625
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
1727
import androidx.compose.runtime.remember
1828
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.runtime.setValue
1930
import androidx.compose.ui.Alignment
2031
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.graphics.vector.ImageVector
2133
import androidx.compose.ui.platform.LocalDensity
2234
import androidx.compose.ui.unit.dp
2335
import androidx.hilt.navigation.compose.hiltViewModel
@@ -26,10 +38,13 @@ import androidx.navigation.compose.currentBackStackEntryAsState
2638
import com.castle.sefirah.navigation.Graph
2739
import com.castle.sefirah.presentation.home.components.AudioDeviceBottomSheet
2840
import com.castle.sefirah.presentation.home.components.DeviceCard
41+
import com.castle.sefirah.presentation.home.components.DeviceControlCard
2942
import com.castle.sefirah.presentation.home.components.MediaCard
3043
import com.castle.sefirah.presentation.home.components.SelectedAudioDevice
44+
import com.castle.sefirah.presentation.home.components.TimerDialog
3145
import com.castle.sefirah.presentation.main.ConnectionViewModel
3246
import kotlinx.coroutines.launch
47+
import sefirah.domain.model.CommandType
3348
import sefirah.domain.model.ConnectionState
3449

3550
@OptIn(ExperimentalMaterial3Api::class)
@@ -49,6 +64,12 @@ fun HomeScreen(
4964
val activeSessions by viewModel.activeSessions.collectAsState()
5065
val audioDevices by viewModel.audioDevices.collectAsState()
5166

67+
var dialogCommand: CommandType? by remember { mutableStateOf(null) }
68+
var showTimerDialog by remember { mutableStateOf(false) }
69+
var hours by remember { mutableStateOf("0") }
70+
var minutes by remember { mutableStateOf("0") }
71+
var seconds by remember { mutableStateOf("0") }
72+
5273
// Bottom sheet state
5374
val scope = rememberCoroutineScope()
5475
val scaffoldState = rememberBottomSheetScaffoldState(
@@ -78,19 +99,27 @@ fun HomeScreen(
7899
verticalArrangement = Arrangement.Top,
79100
) {
80101
item(key = "devices") {
81-
Column(
82-
modifier = Modifier
83-
.fillMaxWidth()
84-
.padding(16.dp)
85-
) {
86-
DeviceCard(
87-
onclick = { deviceDetails?.deviceId?.let {
88-
rootNavController.navigate(route = "device?deviceId=${it}")
89-
} },
90-
device = deviceDetails,
91-
onSyncAction = { connectionViewModel.toggleSync(connectionState == ConnectionState.Disconnected()) },
92-
connectionState = connectionState,
93-
navController = rootNavController
102+
DeviceCard(
103+
onclick = { deviceDetails?.deviceId?.let {
104+
rootNavController.navigate(route = "device?deviceId=${it}")
105+
} },
106+
device = deviceDetails,
107+
onSyncAction = { connectionViewModel.toggleSync(connectionState == ConnectionState.Disconnected()) },
108+
connectionState = connectionState,
109+
navController = rootNavController
110+
)
111+
}
112+
113+
if (connectionState == ConnectionState.Connected) {
114+
item(key = "device_control") {
115+
DeviceControlCard(
116+
onCommandSend = { command ->
117+
viewModel.sendCommand(command, "0")
118+
},
119+
onLongClick = {
120+
dialogCommand = it
121+
showTimerDialog = true
122+
}
94123
)
95124
}
96125
}
@@ -125,7 +154,25 @@ fun HomeScreen(
125154
}
126155
}
127156
}
157+
158+
if (showTimerDialog) {
159+
TimerDialog(
160+
hours = hours,
161+
minutes = minutes,
162+
seconds = seconds,
163+
onHoursChange = { hours = it },
164+
onMinutesChange = { minutes = it },
165+
onSecondsChange = { seconds = it },
166+
onConfirm = {
167+
val totalSeconds = (hours.toIntOrNull() ?: 0) * 3600 + (minutes.toIntOrNull() ?: 0) * 60 + (seconds.toIntOrNull() ?: 0)
168+
if (totalSeconds > 0) {
169+
viewModel.sendCommand(dialogCommand!!, totalSeconds.toString())
170+
}
171+
showTimerDialog = false
172+
},
173+
onDismiss = { showTimerDialog = false }
174+
)
175+
}
128176
}
129177
)
130178
}
131-

app/src/main/java/com/castle/sefirah/presentation/home/HomeViewModel.kt

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import kotlinx.coroutines.Dispatchers
88
import kotlinx.coroutines.flow.StateFlow
99
import kotlinx.coroutines.launch
1010
import sefirah.domain.model.AudioDevice
11+
import sefirah.domain.model.CommandMessage
12+
import sefirah.domain.model.CommandType
1113
import sefirah.domain.model.PlaybackAction
1214
import sefirah.domain.model.PlaybackActionType
1315
import sefirah.domain.model.PlaybackSession
@@ -100,6 +102,17 @@ class HomeViewModel @Inject constructor(
100102
}
101103
}
102104

105+
fun sendCommand(commandType: CommandType, value: String) {
106+
viewModelScope.launch(Dispatchers.IO) {
107+
sendMessage(
108+
CommandMessage(
109+
commandType = commandType,
110+
value = value
111+
)
112+
)
113+
}
114+
}
115+
103116
private suspend fun sendMessage(message: SocketMessage) {
104117
networkManager.sendMessage(message)
105118
}

app/src/main/java/com/castle/sefirah/presentation/home/components/AudioDeviceBottomSheet.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fun SelectedAudioDevice(
5757
Card(
5858
modifier = Modifier
5959
.fillMaxWidth()
60-
.padding(16.dp),
60+
.padding(horizontal = 16.dp, vertical = 8.dp),
6161
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
6262
onClick = { onClick() }
6363
) {

app/src/main/java/com/castle/sefirah/presentation/home/components/DeviceCard.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fun DeviceCard(
5454
onClick = { onclick() },
5555
shape = RoundedCornerShape(16.dp),
5656
colors = CardDefaults.cardColors(),
57-
modifier = modifier
57+
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)
5858
) {
5959
Box(
6060
Modifier.padding(16.dp)) {
@@ -112,6 +112,7 @@ fun DeviceCard(
112112
}
113113
}
114114
}
115+
115116
} ?: EmptyPlaceholder(navController)
116117
}
117118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package com.castle.sefirah.presentation.home.components
2+
3+
import androidx.compose.foundation.combinedClickable
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.width
11+
import androidx.compose.foundation.text.KeyboardOptions
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.automirrored.filled.Logout
14+
import androidx.compose.material.icons.filled.Lock
15+
import androidx.compose.material.icons.filled.PowerSettingsNew
16+
import androidx.compose.material.icons.filled.RestartAlt
17+
import androidx.compose.material.icons.filled.Schedule
18+
import androidx.compose.material3.AlertDialog
19+
import androidx.compose.material3.Button
20+
import androidx.compose.material3.Card
21+
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.OutlinedTextField
24+
import androidx.compose.material3.Text
25+
import androidx.compose.material3.TextButton
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.graphics.vector.ImageVector
30+
import androidx.compose.ui.text.input.KeyboardType
31+
import androidx.compose.ui.unit.dp
32+
import sefirah.domain.model.CommandType
33+
34+
@Composable
35+
fun DeviceControlCard(
36+
onCommandSend: (CommandType) -> Unit,
37+
onLongClick: (CommandType) -> Unit,
38+
modifier: Modifier = Modifier
39+
) {
40+
Card(
41+
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)
42+
) {
43+
Row(
44+
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
45+
horizontalArrangement = Arrangement.SpaceEvenly
46+
) {
47+
DeviceControlButton(
48+
onClick = { onCommandSend(CommandType.Lock) },
49+
onLongClick = { onLongClick(CommandType.Lock) },
50+
icon = Icons.Default.Lock,
51+
contentDescription = "Lock Device"
52+
)
53+
54+
DeviceControlButton(
55+
onClick = { onCommandSend(CommandType.Hibernate) },
56+
onLongClick = { onLongClick(CommandType.Hibernate) },
57+
icon = Icons.Default.Schedule,
58+
contentDescription = "Hibernate Device"
59+
)
60+
61+
62+
DeviceControlButton(
63+
onClick = { onCommandSend(CommandType.Logoff) },
64+
onLongClick = { onLongClick(CommandType.Logoff) },
65+
icon = Icons.AutoMirrored.Filled.Logout,
66+
contentDescription = "Logoff Device"
67+
)
68+
69+
70+
DeviceControlButton(
71+
onClick = { onCommandSend(CommandType.Restart) },
72+
onLongClick = { onLongClick(CommandType.Restart) },
73+
icon = Icons.Default.RestartAlt,
74+
contentDescription = "Restart Device"
75+
)
76+
77+
DeviceControlButton(
78+
onClick = { onCommandSend(CommandType.Shutdown) },
79+
onLongClick = { onLongClick(CommandType.Shutdown) },
80+
icon = Icons.Default.PowerSettingsNew,
81+
contentDescription = "Shutdown Device"
82+
)
83+
}
84+
}
85+
}
86+
87+
@Composable
88+
fun DeviceControlButton(
89+
onClick: () -> Unit,
90+
onLongClick: () -> Unit,
91+
icon: ImageVector,
92+
contentDescription: String?,
93+
) {
94+
Icon(
95+
imageVector = icon,
96+
contentDescription = contentDescription,
97+
tint = MaterialTheme.colorScheme.surfaceTint,
98+
modifier = Modifier.combinedClickable(
99+
onClick = onClick,
100+
onLongClick = onLongClick,
101+
)
102+
)
103+
}
104+
105+
106+
@Composable
107+
fun TimerDialog(
108+
hours: String,
109+
minutes: String,
110+
seconds: String,
111+
onHoursChange: (String) -> Unit,
112+
onMinutesChange: (String) -> Unit,
113+
onSecondsChange: (String) -> Unit,
114+
onConfirm: () -> Unit,
115+
onDismiss: () -> Unit
116+
) {
117+
AlertDialog(
118+
onDismissRequest = onDismiss,
119+
title = { Text("Set timer") },
120+
text = {
121+
Column {
122+
Row(
123+
modifier = Modifier.fillMaxWidth(),
124+
horizontalArrangement = Arrangement.SpaceEvenly,
125+
verticalAlignment = Alignment.CenterVertically
126+
) {
127+
OutlinedTextField(
128+
value = hours,
129+
onValueChange = { onHoursChange(it.filter { char -> char.isDigit() }) },
130+
label = { Text("Hours") },
131+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
132+
modifier = Modifier.weight(1f)
133+
)
134+
135+
Spacer(modifier = Modifier.width(8.dp))
136+
137+
OutlinedTextField(
138+
value = minutes,
139+
onValueChange = { onMinutesChange(it.filter { char -> char.isDigit() }) },
140+
label = { Text("Minutes") },
141+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
142+
modifier = Modifier.weight(1f)
143+
)
144+
145+
Spacer(modifier = Modifier.width(8.dp))
146+
147+
OutlinedTextField(
148+
value = seconds,
149+
onValueChange = { onSecondsChange(it.filter { char -> char.isDigit() }) },
150+
label = { Text("Seconds") },
151+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
152+
modifier = Modifier.weight(1f)
153+
)
154+
}
155+
}
156+
},
157+
confirmButton = {
158+
Button(onClick = onConfirm) {
159+
Text("Confirm")
160+
}
161+
},
162+
dismissButton = {
163+
TextButton(onClick = onDismiss) {
164+
Text("Cancel")
165+
}
166+
}
167+
)
168+
}

app/src/main/java/com/castle/sefirah/presentation/home/components/MediaCard.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ fun PlaybackSession(
117117
Card(
118118
modifier = modifier
119119
.fillMaxWidth()
120-
.padding(16.dp),
120+
.padding(horizontal = 16.dp, vertical = 8.dp),
121121
shape = RoundedCornerShape(16.dp),
122122
) {
123123
session.trackTitle?.let {

app/src/main/java/com/castle/sefirah/presentation/home/components/NotificationCard.kt

-9
This file was deleted.

0 commit comments

Comments
 (0)