[ADDED] #40 basic UI for selecting BYOS type device

This commit is contained in:
Hossain Khan
2025-05-08 00:19:04 -04:00
parent 1d14defc24
commit 74cde83558
5 changed files with 167 additions and 1 deletions
@@ -0,0 +1,27 @@
package ink.trmnl.android.model
/**
* Enum class to represent the type of TRMNL device and service.
* - https://usetrmnl.com/developers
*/
enum class TrmnlDeviceType {
/**
* Represents a TRMNL device.
*/
TRMNL,
/**
* Represents a Bring Your Own Device.
* Build your own device with our modified firmware connecting to TRMNL servers.
*
* - Full access to TRMNL web app
* - Plugin API access for DIY devices
*/
BYOD,
/**
* Represents a Full DIY Stack - BYOD/S.
* Bring your own device AND server.
*/
BYOS,
}
@@ -1,5 +1,10 @@
package ink.trmnl.android.ui.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -37,6 +42,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -89,6 +97,7 @@ import ink.trmnl.android.data.RepositoryConfigProvider
import ink.trmnl.android.data.TrmnlDisplayRepository
import ink.trmnl.android.data.TrmnlTokenDataStore
import ink.trmnl.android.di.AppScope
import ink.trmnl.android.model.TrmnlDeviceType
import ink.trmnl.android.ui.display.TrmnlMirrorDisplayScreen
import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult
import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult.Failure
@@ -125,6 +134,8 @@ data class AppSettingsScreen(
val returnToMirrorAfterSave: Boolean = false,
) : Screen {
data class State(
val deviceType: TrmnlDeviceType,
val serverBaseUrl: String,
val accessToken: String,
val usesFakeApiData: Boolean,
val isLoading: Boolean = false,
@@ -174,6 +185,14 @@ data class AppSettingsScreen(
* Event triggered to cancel the scheduled work.
*/
data object CancelScheduledWork : Event()
data class DeviceTypeChanged(
val type: TrmnlDeviceType,
) : Event()
data class ServerUrlChanged(
val url: String,
) : Event()
}
}
@@ -194,6 +213,8 @@ class AppSettingsPresenter
) : Presenter<AppSettingsScreen.State> {
@Composable
override fun present(): AppSettingsScreen.State {
var deviceType by remember { mutableStateOf(TrmnlDeviceType.TRMNL) }
var serverUrl by remember { mutableStateOf("") }
var accessToken by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var validationResult by remember { mutableStateOf<ValidationResult?>(null) }
@@ -217,6 +238,8 @@ class AppSettingsPresenter
}
return AppSettingsScreen.State(
deviceType = deviceType,
serverBaseUrl = serverUrl,
accessToken = accessToken,
usesFakeApiData = usesFakeApiData,
isLoading = isLoading,
@@ -262,7 +285,7 @@ class AppSettingsPresenter
AppSettingsScreen.Event.SaveAndContinue -> {
// Only save if validation was successful
val result = validationResult
if (result is ValidationResult.Success) {
if (result is Success) {
scope.launch {
trmnlTokenDataStore.saveAccessToken(accessToken)
trmnlWorkScheduler.updateRefreshInterval(result.refreshRateSecs)
@@ -283,6 +306,14 @@ class AppSettingsPresenter
AppSettingsScreen.Event.CancelScheduledWork -> {
trmnlWorkScheduler.cancelImageRefreshWork()
}
is AppSettingsScreen.Event.DeviceTypeChanged -> {
deviceType = event.type
}
is AppSettingsScreen.Event.ServerUrlChanged -> {
serverUrl = event.url
}
}
},
)
@@ -395,6 +426,14 @@ fun AppSettingsContent(
Spacer(modifier = Modifier.height(24.dp))
DeviceTypeSelectorConfig(
selectedType = state.deviceType,
serverUrl = state.serverBaseUrl,
onTypeSelected = { state.eventSink(AppSettingsScreen.Event.DeviceTypeChanged(it)) },
onServerUrlChanged = { state.eventSink(AppSettingsScreen.Event.ServerUrlChanged(it)) },
modifier = Modifier.padding(bottom = 16.dp),
)
// Password field with toggle visibility button
OutlinedTextField(
value = state.accessToken,
@@ -517,6 +556,69 @@ fun AppSettingsContent(
}
}
@Composable
private fun DeviceTypeSelectorConfig(
selectedType: TrmnlDeviceType,
serverUrl: String = "",
onTypeSelected: (TrmnlDeviceType) -> Unit,
onServerUrlChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth()) {
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
TrmnlDeviceType.entries.forEachIndexed { index, deviceType ->
SegmentedButton(
shape =
SegmentedButtonDefaults.itemShape(
index = index,
count = TrmnlDeviceType.entries.size,
),
icon = {
SegmentedButtonDefaults.Icon(active = deviceType == selectedType) {
Icon(
painter =
when (deviceType) {
TrmnlDeviceType.TRMNL -> painterResource(R.drawable.trmnl_logo_plain)
TrmnlDeviceType.BYOD -> painterResource(R.drawable.devices_24dp)
TrmnlDeviceType.BYOS -> painterResource(R.drawable.storage_24dp)
},
contentDescription = null,
)
}
},
onClick = { onTypeSelected(deviceType) },
selected = deviceType == selectedType,
) {
Text(deviceType.name)
}
}
}
AnimatedVisibility(
visible = selectedType == TrmnlDeviceType.BYOS,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
OutlinedTextField(
value = serverUrl,
onValueChange = onServerUrlChanged,
label = { Text("API Server Base URL") },
placeholder = { Text("https://your-trmnl-server.com") },
modifier =
Modifier
.fillMaxWidth()
.padding(top = 16.dp),
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Done,
),
singleLine = true,
)
}
}
}
/**
* Displays the status of the TRMNL display image refresh schedule.
*
@@ -739,6 +841,8 @@ private fun PreviewAppSettingsContentInitial() {
AppSettingsContent(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "",
usesFakeApiData = true,
isLoading = false,
@@ -757,6 +861,8 @@ private fun PreviewAppSettingsContentLoading() {
AppSettingsContent(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "some-token",
usesFakeApiData = false,
isLoading = true,
@@ -775,6 +881,8 @@ private fun PreviewAppSettingsContentSuccess() {
AppSettingsContent(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "valid-token-123",
usesFakeApiData = false,
isLoading = false,
@@ -797,6 +905,8 @@ private fun PreviewAppSettingsContentFailure() {
AppSettingsContent(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "invalid-token",
usesFakeApiData = false,
isLoading = false,
@@ -822,6 +932,8 @@ private fun PreviewAppSettingsContentWithWork() {
AppSettingsContent(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "valid-token-123",
usesFakeApiData = false,
isLoading = false,
@@ -850,6 +962,8 @@ private fun PreviewWorkScheduleStatusCardScheduled() {
WorkScheduleStatusCard(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "some-token",
usesFakeApiData = false,
nextRefreshJobInfo =
@@ -872,6 +986,8 @@ private fun PreviewWorkScheduleStatusCardNoWork() {
WorkScheduleStatusCard(
state =
AppSettingsScreen.State(
deviceType = TrmnlDeviceType.TRMNL,
serverBaseUrl = "https://example.com",
accessToken = "some-token",
usesFakeApiData = false,
nextRefreshJobInfo = null,
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M80,800v-120h80v-440q0,-33 23.5,-56.5T240,160h600v80L240,240v440h240v120L80,800ZM600,800q-17,0 -28.5,-11.5T560,760v-400q0,-17 11.5,-28.5T600,320h240q17,0 28.5,11.5T880,360v400q0,17 -11.5,28.5T840,800L600,800ZM640,680h160v-280L640,400v280ZM640,680h160,-160Z"
android:fillColor="#e8eaed"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M120,800v-160h720v160L120,800ZM200,760h80v-80h-80v80ZM120,320v-160h720v160L120,320ZM200,280h80v-80h-80v80ZM120,560v-160h720v160L120,560ZM200,520h80v-80h-80v80Z"
android:fillColor="#e8eaed"/>
</vector>
+5
View File
@@ -17,6 +17,11 @@ kotlinxCoroutinesTest = "1.7.3"
ksp = "2.1.10-1.0.31"
lifecycleRuntimeKtx = "2.8.7"
# Added full icon set - release build will shrink unused icons
# https://developer.android.com/jetpack/androidx/releases/compose-material
# https://developer.android.com/reference/kotlin/androidx/compose/material/icons/package-summary
materialIcons = "1.7.8"
# Kotlin linter with built-in formatter
# https://github.com/jeremymailen/kotlinter-gradle
# https://github.com/pinterest/ktlint