mirror of
https://github.com/usetrmnl/trmnl-android.git
synced 2026-04-29 13:35:26 -07:00
[ADDED] #40 basic UI for selecting BYOS type device
This commit is contained in:
@@ -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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user