2 Commits

Author SHA1 Message Date
tkrmagid
067170802a feat: remove home refresh, admin list refresh, app name in OS (v0.3.2) 2026-08-05 17:16:28 +09:00
tkrmagid
2287e1cf83 feat: app-level display name (v0.3.1) 2026-08-05 14:45:57 +09:00
7 changed files with 78 additions and 17 deletions

1
.gitignore vendored
View File

@@ -12,4 +12,3 @@
.cxx
app/build/
build/
.kotlin/

View File

@@ -24,8 +24,8 @@ android {
applicationId = "kr.tkrmagid.easyappliance"
minSdk = 24
targetSdk = 35
versionCode = 3
versionName = "0.3.0"
versionCode = 5
versionName = "0.3.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -17,6 +17,8 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
/** User/admin choices that must survive app restarts. */
data class AppSettings(
/** App-level display name shown as the home header; null = default. */
val appName: String? = null,
val selectedDeviceId: String? = null,
val showRemainingTime: Boolean = true,
val showReservation: Boolean = true,
@@ -36,6 +38,7 @@ class SettingsRepository(private val context: Context) {
val settings: Flow<AppSettings> = context.dataStore.data.map { p ->
AppSettings(
appName = p[KEY_APP_NAME]?.takeIf { it.isNotBlank() },
selectedDeviceId = p[KEY_SELECTED_DEVICE],
showRemainingTime = p[KEY_SHOW_REMAINING] ?: true,
showReservation = p[KEY_SHOW_RESERVATION] ?: true,
@@ -47,6 +50,9 @@ class SettingsRepository(private val context: Context) {
)
}
suspend fun setAppName(name: String) =
edit { it[KEY_APP_NAME] = name.trim() }
suspend fun setSelectedDevice(deviceId: String) =
edit { it[KEY_SELECTED_DEVICE] = deviceId }
@@ -93,6 +99,7 @@ class SettingsRepository(private val context: Context) {
else runCatching { Json.decodeFromString<Map<String, DeviceMacro>>(raw) }.getOrDefault(emptyMap())
private companion object {
val KEY_APP_NAME = stringPreferencesKey("app_name")
val KEY_SELECTED_DEVICE = stringPreferencesKey("selected_device_id")
val KEY_SHOW_REMAINING = booleanPreferencesKey("show_remaining_time")
val KEY_SHOW_RESERVATION = booleanPreferencesKey("show_reservation")

View File

@@ -18,10 +18,13 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
@@ -49,6 +52,7 @@ import kr.tkrmagid.easyappliance.data.DeviceImages
import kr.tkrmagid.easyappliance.data.DeviceMacro
import kr.tkrmagid.easyappliance.data.ImageStorage
import kr.tkrmagid.easyappliance.vm.HomeUiState
import kr.tkrmagid.easyappliance.vm.appDisplayName
import kr.tkrmagid.easyappliance.vm.displayLabelFor
import kr.tkrmagid.easyappliance.vm.imageRefFor
import kr.tkrmagid.easyappliance.vm.macroFor
@@ -57,6 +61,8 @@ import kr.tkrmagid.easyappliance.vm.macroFor
fun AdminScreen(
state: HomeUiState,
onSelectDevice: (String) -> Unit,
onRefreshDevices: () -> Unit,
onSetAppName: (String) -> Unit,
onToggleRemaining: (Boolean) -> Unit,
onToggleReservation: (Boolean) -> Unit,
onTogglePowerControls: (Boolean) -> Unit,
@@ -78,6 +84,13 @@ fun AdminScreen(
Text("관리자 설정", style = MaterialTheme.typography.headlineLarge)
Spacer(Modifier.height(24.dp))
SectionTitle("앱 이름 바꾸기")
NameEditor(
current = state.appDisplayName(),
onSave = onSetAppName,
)
Spacer(Modifier.height(28.dp))
SectionTitle("조작할 기기 선택")
Card(
modifier = Modifier.fillMaxWidth(),
@@ -102,9 +115,18 @@ fun AdminScreen(
}
}
Spacer(Modifier.height(12.dp))
OutlinedButton(
onClick = onRefreshDevices,
modifier = Modifier.fillMaxWidth().height(60.dp),
) {
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(26.dp))
Text(" 목록 새로고침", style = MaterialTheme.typography.bodyLarge)
}
if (selected != null) {
Spacer(Modifier.height(28.dp))
SectionTitle("이름 바꾸기")
SectionTitle("기기 이름 바꾸기")
NameEditor(
current = state.displayLabelFor(selected),
onSave = { onSetLabel(selected.id, it) },

View File

@@ -1,5 +1,9 @@
package kr.tkrmagid.easyappliance.ui
import android.app.ActivityManager
import android.content.Context
import android.content.ContextWrapper
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -12,17 +16,25 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kr.tkrmagid.easyappliance.data.koreanLabel
import kr.tkrmagid.easyappliance.vm.AppViewModel
import kr.tkrmagid.easyappliance.vm.appDisplayName
import kr.tkrmagid.easyappliance.vm.displayLabelFor
import kotlinx.coroutines.delay
private enum class Screen { HOME, ADMIN }
private tailrec fun Context.findActivity(): ComponentActivity? = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
private const val POLL_OK_MS = 15_000L
private const val POLL_OFFLINE_MS = 5_000L
@@ -32,6 +44,16 @@ fun AppRoot() {
val state by vm.state.collectAsStateWithLifecycle()
var screen by rememberSaveable { mutableStateOf(Screen.HOME) }
// Reflect the custom app name in the OS: the label shown in the Recents/task
// switcher updates at runtime (the launcher icon label stays fixed by Android).
val context = LocalContext.current
val appName = state.appDisplayName()
LaunchedEffect(appName) {
context.findActivity()?.setTaskDescription(
ActivityManager.TaskDescription(appName),
)
}
// Refresh when returning to the foreground...
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { vm.refresh() }
// ...and poll while composed; retry faster while disconnected so the warning
@@ -55,7 +77,6 @@ fun AppRoot() {
state = state,
onTogglePower = vm::togglePower,
onRunMacro = vm::runMacro,
onRefresh = vm::refresh,
modifier = Modifier
.fillMaxSize()
.adminUnlockGesture { screen = Screen.ADMIN },
@@ -63,6 +84,8 @@ fun AppRoot() {
Screen.ADMIN -> AdminScreen(
state = state,
onSelectDevice = vm::selectDevice,
onRefreshDevices = vm::refreshDevices,
onSetAppName = vm::setAppName,
onToggleRemaining = vm::setShowRemainingTime,
onToggleReservation = vm::setShowReservation,
onTogglePowerControls = vm::setShowPowerControls,

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
@@ -23,7 +22,6 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -34,6 +32,7 @@ import androidx.compose.ui.unit.dp
import kr.tkrmagid.easyappliance.data.DeviceType
import androidx.compose.material.icons.filled.PlayArrow
import kr.tkrmagid.easyappliance.vm.HomeUiState
import kr.tkrmagid.easyappliance.vm.appDisplayName
import kr.tkrmagid.easyappliance.vm.displayLabelFor
import kr.tkrmagid.easyappliance.vm.imageRefFor
import kr.tkrmagid.easyappliance.vm.macroFor
@@ -45,7 +44,6 @@ fun HomeScreen(
state: HomeUiState,
onTogglePower: (Boolean) -> Unit,
onRunMacro: () -> Unit,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
@@ -71,6 +69,13 @@ fun HomeScreen(
modifier = Modifier.widthIn(max = contentMaxWidth).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = state.appDisplayName(),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(16.dp))
DeviceImage(
imageRef = state.imageRefFor(device),
modifier = Modifier.size(if (narrow) 96.dp else 128.dp),
@@ -102,15 +107,6 @@ fun HomeScreen(
)
}
}
Spacer(Modifier.height(20.dp))
OutlinedButton(
onClick = onRefresh,
modifier = Modifier.fillMaxWidth().height(BigButtonHeight),
) {
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(30.dp))
Text(" 새로고침", style = MaterialTheme.typography.labelLarge)
}
}
}
}

View File

@@ -123,6 +123,14 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
viewModelScope.launch { loadStatus(id) }
}
/** Reload the device list from the repository (admin "목록 새로고침"). */
fun refreshDevices() {
viewModelScope.launch {
loadDevices()
resolveSelectionAndStatus(_state.value.settings)
}
}
/** "다시 연결하기" — clear preview flag and re-attempt list + status. */
fun retryConnection() {
forcedWarning = false
@@ -170,6 +178,7 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
viewModelScope.launch { settingsRepo.setDeviceMacro(deviceId, macro) }
}
fun setAppName(name: String) { viewModelScope.launch { settingsRepo.setAppName(name) } }
fun setShowRemainingTime(value: Boolean) { viewModelScope.launch { settingsRepo.setShowRemainingTime(value) } }
fun setShowReservation(value: Boolean) { viewModelScope.launch { settingsRepo.setShowReservation(value) } }
fun setShowPowerControls(value: Boolean) { viewModelScope.launch { settingsRepo.setShowPowerControls(value) } }
@@ -188,3 +197,8 @@ fun HomeUiState.imageRefFor(device: Device): String =
/** The device's macro, or null if none configured. */
fun HomeUiState.macroFor(device: Device): DeviceMacro? = settings.deviceMacros[device.id]
const val DEFAULT_APP_NAME = "쉬운 가전 리모컨"
/** App-level display name shown as the home header. */
fun HomeUiState.appDisplayName(): String = settings.appName ?: DEFAULT_APP_NAME