Improve Android profile management and account settings UI.
Add profile update API calls and richer edit/manage flows on TV and mobile, with version bumps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6a0eee0313
commit
2eabd76d93
8 changed files with 1168 additions and 579 deletions
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster"
|
applicationId = "nl.vonas.mediacluster"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 9
|
versionCode = 11
|
||||||
versionName = "1.0.8"
|
versionName = "1.0.10"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,26 @@ class ApiClient(
|
||||||
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateProfile(
|
||||||
|
profileId: String,
|
||||||
|
name: String? = null,
|
||||||
|
avatarId: String? = null,
|
||||||
|
isKids: Boolean? = null,
|
||||||
|
pin: String? = null,
|
||||||
|
clearPin: Boolean = false,
|
||||||
|
): ViewerProfile = withContext(Dispatchers.IO) {
|
||||||
|
val body = JSONObject()
|
||||||
|
if (name != null) body.put("name", name)
|
||||||
|
if (avatarId != null) body.put("avatarId", avatarId)
|
||||||
|
if (isKids != null) body.put("isKids", isKids)
|
||||||
|
if (clearPin) body.put("clearPin", true)
|
||||||
|
else if (!pin.isNullOrBlank()) body.put("pin", pin)
|
||||||
|
val req = authed("$baseUrl/api/v1/client/profiles/$profileId")
|
||||||
|
.patch(body.toString().toRequestBody(jsonMedia))
|
||||||
|
.build()
|
||||||
|
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun deleteProfile(profileId: String) = withContext(Dispatchers.IO) {
|
suspend fun deleteProfile(profileId: String) = withContext(Dispatchers.IO) {
|
||||||
val req = authed("$baseUrl/api/v1/client/profiles/$profileId").delete().build()
|
val req = authed("$baseUrl/api/v1/client/profiles/$profileId").delete().build()
|
||||||
execute(req)
|
execute(req)
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,8 @@ private fun MainHub(
|
||||||
)
|
)
|
||||||
MainTab.Settings -> SettingsScreen(
|
MainTab.Settings -> SettingsScreen(
|
||||||
api = api,
|
api = api,
|
||||||
|
profileAvatarId = profileAvatarId,
|
||||||
|
profileName = profileName,
|
||||||
onBack = {
|
onBack = {
|
||||||
onTabChange(MainTab.Home)
|
onTabChange(MainTab.Home)
|
||||||
focusTopNav()
|
focusTopNav()
|
||||||
|
|
@ -509,38 +511,6 @@ private fun MainHub(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StatusBarGlassOverlay(Modifier.align(Alignment.TopCenter))
|
StatusBarGlassOverlay(Modifier.align(Alignment.TopCenter))
|
||||||
// Profiel-avatar rechtsboven → terug naar switcher
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.statusBarsPadding()
|
|
||||||
.padding(top = 8.dp, end = 12.dp)
|
|
||||||
.clip(RoundedCornerShape(20.dp))
|
|
||||||
.background(Color(0xCC1A1A22))
|
|
||||||
.clickable(onClick = onSwitchProfile)
|
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(28.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color(0xFF2E2E3A)),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(avatarEmoji(profileAvatarId ?: "fox"), fontSize = 16.sp)
|
|
||||||
}
|
|
||||||
if (!profileName.isNullOrBlank()) {
|
|
||||||
Text(
|
|
||||||
profileName,
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Box(Modifier.focusRequester(navFocus)) {
|
Box(Modifier.focusRequester(navFocus)) {
|
||||||
TopNavBar(
|
TopNavBar(
|
||||||
|
|
@ -1117,6 +1087,8 @@ private fun ShelfBlock(
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
profileAvatarId: String? = null,
|
||||||
|
profileName: String? = null,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSwitchProfile: () -> Unit = {},
|
onSwitchProfile: () -> Unit = {},
|
||||||
onManageProfiles: () -> Unit = {},
|
onManageProfiles: () -> Unit = {},
|
||||||
|
|
@ -1189,20 +1161,40 @@ private fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Box(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(McColors.BgElevated)
|
.background(McColors.BgElevated)
|
||||||
.clickable(onClick = onSwitchProfile)
|
.clickable(onClick = onSwitchProfile)
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Box(
|
||||||
"Profiel wisselen",
|
Modifier
|
||||||
color = Color.White,
|
.size(44.dp)
|
||||||
fontSize = McDimens.body,
|
.clip(CircleShape)
|
||||||
fontWeight = FontWeight.SemiBold,
|
.background(Color(0xFF2E2E3A)),
|
||||||
)
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(avatarEmoji(profileAvatarId ?: "fox"), fontSize = 22.sp)
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
profileName?.takeIf { it.isNotBlank() } ?: "Profiel",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = McDimens.body,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Tik om van profiel te wisselen",
|
||||||
|
color = McColors.TextDim,
|
||||||
|
fontSize = McDimens.meta,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text("›", color = Color(0xFF888888), fontSize = 22.sp)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 71
|
versionCode = 74
|
||||||
versionName = "0.14.8"
|
versionName = "0.14.11"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,26 @@ class ApiClient(
|
||||||
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateProfile(
|
||||||
|
profileId: String,
|
||||||
|
name: String? = null,
|
||||||
|
avatarId: String? = null,
|
||||||
|
isKids: Boolean? = null,
|
||||||
|
pin: String? = null,
|
||||||
|
clearPin: Boolean = false,
|
||||||
|
): ViewerProfile = withContext(Dispatchers.IO) {
|
||||||
|
val body = JSONObject()
|
||||||
|
if (name != null) body.put("name", name)
|
||||||
|
if (avatarId != null) body.put("avatarId", avatarId)
|
||||||
|
if (isKids != null) body.put("isKids", isKids)
|
||||||
|
if (clearPin) body.put("clearPin", true)
|
||||||
|
else if (!pin.isNullOrBlank()) body.put("pin", pin)
|
||||||
|
val req = authed("$baseUrl/api/v1/client/profiles/$profileId")
|
||||||
|
.patch(body.toString().toRequestBody(jsonMedia))
|
||||||
|
.build()
|
||||||
|
parseObject(execute(req)) { o -> o.getJSONObject("profile").toProfile() }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun deleteProfile(profileId: String) = withContext(Dispatchers.IO) {
|
suspend fun deleteProfile(profileId: String) = withContext(Dispatchers.IO) {
|
||||||
val req = authed("$baseUrl/api/v1/client/profiles/$profileId").delete().build()
|
val req = authed("$baseUrl/api/v1/client/profiles/$profileId").delete().build()
|
||||||
execute(req)
|
execute(req)
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
@ -57,6 +59,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -628,6 +631,8 @@ private fun MainHub(
|
||||||
)
|
)
|
||||||
MainTab.Settings -> SettingsScreen(
|
MainTab.Settings -> SettingsScreen(
|
||||||
api = api,
|
api = api,
|
||||||
|
profileAvatarId = profileAvatarId,
|
||||||
|
profileName = profileName,
|
||||||
onBack = {
|
onBack = {
|
||||||
onTabChange(MainTab.Home)
|
onTabChange(MainTab.Home)
|
||||||
focusTopNav()
|
focusTopNav()
|
||||||
|
|
@ -1329,6 +1334,8 @@ private fun ShelfBlock(
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
profileAvatarId: String? = null,
|
||||||
|
profileName: String? = null,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSwitchProfile: () -> Unit = {},
|
onSwitchProfile: () -> Unit = {},
|
||||||
onManageProfiles: () -> Unit = {},
|
onManageProfiles: () -> Unit = {},
|
||||||
|
|
@ -1337,6 +1344,7 @@ private fun SettingsScreen(
|
||||||
var prefs by remember { mutableStateOf<List<ShelfPref>>(emptyList()) }
|
var prefs by remember { mutableStateOf<List<ShelfPref>>(emptyList()) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val firstFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1344,6 +1352,7 @@ private fun SettingsScreen(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e.message
|
error = e.message
|
||||||
}
|
}
|
||||||
|
runCatching { firstFocus.requestFocus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persist(next: List<ShelfPref>) {
|
fun persist(next: List<ShelfPref>) {
|
||||||
|
|
@ -1357,101 +1366,271 @@ private fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
Box(
|
||||||
Modifier.fillMaxSize().background(McColors.Bg).padding(40.dp),
|
Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color(0xFF0B0B0F), Color(0xFF12121A), Color(0xFF0B0B0F)),
|
||||||
|
),
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
item {
|
LazyColumn(
|
||||||
Text("Instellingen", color = Color.White, fontSize = 30.sp, fontWeight = FontWeight.Bold)
|
Modifier
|
||||||
Spacer(Modifier.height(16.dp))
|
.fillMaxSize()
|
||||||
SectionLabel("Account")
|
.padding(horizontal = 48.dp, vertical = 28.dp),
|
||||||
Spacer(Modifier.height(8.dp))
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
Column(
|
contentPadding = PaddingValues(bottom = 40.dp),
|
||||||
Modifier
|
) {
|
||||||
.fillMaxWidth()
|
item {
|
||||||
.clip(RoundedCornerShape(14.dp))
|
Text(
|
||||||
.background(McColors.BgElevated)
|
"Instellingen",
|
||||||
.padding(20.dp),
|
color = Color.White,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
fontSize = 32.sp,
|
||||||
) {
|
fontWeight = FontWeight.Bold,
|
||||||
Text("Vonas Media Cluster", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
)
|
||||||
Text("Versie ${BuildConfig.VERSION_NAME}", color = McColors.TextDim, fontSize = 14.sp)
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
"Profiel, account en home-indeling",
|
||||||
|
color = Color(0xFF8E8E9A),
|
||||||
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(22.dp))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
McButton("Profiel wisselen", onClick = onSwitchProfile)
|
item {
|
||||||
Spacer(Modifier.height(12.dp))
|
SectionLabel("Profiel")
|
||||||
McButton("Profielen beheren", onClick = onManageProfiles)
|
Spacer(Modifier.height(10.dp))
|
||||||
Spacer(Modifier.height(12.dp))
|
Surface(
|
||||||
McButton("Uitloggen", onClick = onLogout)
|
onClick = onSwitchProfile,
|
||||||
Spacer(Modifier.height(20.dp))
|
modifier = Modifier
|
||||||
SectionLabel("Home-planken")
|
.fillMaxWidth()
|
||||||
Spacer(Modifier.height(4.dp))
|
.focusRequester(firstFocus),
|
||||||
Text(
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(16.dp)),
|
||||||
"Kies welke rijen op Home staan en in welke volgorde.",
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.02f),
|
||||||
color = McColors.TextDim,
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
fontSize = 15.sp,
|
containerColor = Color(0xFF1A1A22),
|
||||||
)
|
focusedContainerColor = Color(0xFF262632),
|
||||||
if (error != null) {
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 22.dp, vertical = 18.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFF2E2E3A)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
avatarEmoji(profileAvatarId ?: "fox"),
|
||||||
|
fontSize = 30.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
profileName?.takeIf { it.isNotBlank() } ?: "Profiel",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
"Wissel van kijkprofiel",
|
||||||
|
color = Color(0xFF9A9AA8),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text("›", color = Color(0xFF888888), fontSize = 28.sp, fontWeight = FontWeight.Light)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
SettingsActionRow(
|
||||||
|
title = "Profielen beheren",
|
||||||
|
subtitle = "Toevoegen, bewerken of verwijderen",
|
||||||
|
onClick = onManageProfiles,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
SectionLabel("Account")
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color(0xFF1A1A22))
|
||||||
|
.padding(horizontal = 22.dp, vertical = 18.dp),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Vonas Media Cluster",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
"Versie ${BuildConfig.VERSION_NAME}",
|
||||||
|
color = Color(0xFF8E8E9A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
SettingsActionRow(
|
||||||
|
title = "Uitloggen",
|
||||||
|
subtitle = "Apparaat ontkoppelen van dit account",
|
||||||
|
danger = true,
|
||||||
|
onClick = onLogout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
SectionLabel("Home-planken")
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
"Kies welke rijen op Home staan en in welke volgorde.",
|
||||||
|
color = Color(0xFF8E8E9A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
if (error != null) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(error!!, color = McColors.Danger, fontSize = 14.sp)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(error!!, color = McColors.Danger)
|
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
}
|
itemsIndexed(prefs) { index, p ->
|
||||||
itemsIndexed(prefs) { index, p ->
|
Surface(
|
||||||
Row(
|
onClick = {
|
||||||
Modifier
|
val next = prefs.toMutableList()
|
||||||
.fillMaxWidth()
|
next[index] = p.copy(visible = !p.visible)
|
||||||
.clip(RoundedCornerShape(14.dp))
|
persist(next)
|
||||||
.background(McColors.BgElevated)
|
},
|
||||||
.padding(16.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(14.dp)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.015f),
|
||||||
) {
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
Column(Modifier.weight(1f)) {
|
containerColor = Color(0xFF1A1A22),
|
||||||
Text(p.name, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
|
focusedContainerColor = Color(0xFF262632),
|
||||||
Text(
|
),
|
||||||
if (p.visible) "Zichtbaar op Home" else "Verborgen",
|
) {
|
||||||
color = if (p.visible) Color(0xFF888888) else Color(0xFF555555),
|
Row(
|
||||||
fontSize = 13.sp,
|
Modifier
|
||||||
)
|
.fillMaxWidth()
|
||||||
}
|
.padding(horizontal = 18.dp, vertical = 14.dp),
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
McButton("↑") {
|
) {
|
||||||
if (index > 0) {
|
Column(Modifier.weight(1f)) {
|
||||||
val next = prefs.toMutableList()
|
Text(
|
||||||
val tmp = next[index - 1]
|
p.name,
|
||||||
next[index - 1] = next[index]
|
color = Color.White,
|
||||||
next[index] = tmp
|
fontSize = 17.sp,
|
||||||
persist(next)
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(3.dp))
|
||||||
|
Text(
|
||||||
|
if (p.visible) "Zichtbaar op Home" else "Verborgen",
|
||||||
|
color = if (p.visible) Color(0xFF8E8E9A) else Color(0xFF555555),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
McButton("↑") {
|
||||||
|
if (index > 0) {
|
||||||
|
val next = prefs.toMutableList()
|
||||||
|
val tmp = next[index - 1]
|
||||||
|
next[index - 1] = next[index]
|
||||||
|
next[index] = tmp
|
||||||
|
persist(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
McButton("↓") {
|
||||||
|
if (index < prefs.lastIndex) {
|
||||||
|
val next = prefs.toMutableList()
|
||||||
|
val tmp = next[index + 1]
|
||||||
|
next[index + 1] = next[index]
|
||||||
|
next[index] = tmp
|
||||||
|
persist(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = p.visible,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
val next = prefs.toMutableList()
|
||||||
|
next[index] = p.copy(visible = checked)
|
||||||
|
persist(next)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
McButton("↓") {
|
|
||||||
if (index < prefs.lastIndex) {
|
|
||||||
val next = prefs.toMutableList()
|
|
||||||
val tmp = next[index + 1]
|
|
||||||
next[index + 1] = next[index]
|
|
||||||
next[index] = tmp
|
|
||||||
persist(next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Switch(
|
|
||||||
checked = p.visible,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
val next = prefs.toMutableList()
|
|
||||||
next[index] = p.copy(visible = checked)
|
|
||||||
persist(next)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
Text(
|
||||||
|
"Vonas Media Cluster · ${BuildConfig.VERSION_NAME}",
|
||||||
|
color = Color(0xFF44444E),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
item {
|
}
|
||||||
Spacer(Modifier.height(12.dp))
|
}
|
||||||
Text(
|
|
||||||
"Vonas Media Cluster · ${BuildConfig.VERSION_NAME}",
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
color = Color(0xFF555555),
|
@Composable
|
||||||
fontSize = 12.sp,
|
private fun SettingsActionRow(
|
||||||
)
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
danger: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(14.dp)),
|
||||||
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.02f),
|
||||||
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
|
containerColor = Color(0xFF1A1A22),
|
||||||
|
focusedContainerColor = Color(0xFF262632),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 22.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
color = if (danger) McColors.Danger else Color.White,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(3.dp))
|
||||||
|
Text(subtitle, color = Color(0xFF8E8E9A), fontSize = 13.sp)
|
||||||
|
}
|
||||||
|
Text("›", color = Color(0xFF888888), fontSize = 26.sp, fontWeight = FontWeight.Light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -35,21 +36,21 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nl.vonas.mediacluster.tv.data.ApiClient
|
import nl.vonas.mediacluster.tv.data.ApiClient
|
||||||
import nl.vonas.mediacluster.tv.data.SessionStore
|
import nl.vonas.mediacluster.tv.data.SessionStore
|
||||||
import nl.vonas.mediacluster.tv.data.ViewerProfile
|
import nl.vonas.mediacluster.tv.data.ViewerProfile
|
||||||
import nl.vonas.mediacluster.tv.ui.theme.McColors
|
import nl.vonas.mediacluster.tv.ui.theme.McColors
|
||||||
|
|
||||||
private enum class ProfilesMode { Pick, Pin, Create, DeleteConfirm }
|
private enum class ProfilesMode { Pick, Pin, Create, Edit, DeleteConfirm }
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -71,8 +72,22 @@ fun ProfilesScreen(
|
||||||
var createAvatar by remember { mutableStateOf("fox") }
|
var createAvatar by remember { mutableStateOf("fox") }
|
||||||
var createKids by remember { mutableStateOf(false) }
|
var createKids by remember { mutableStateOf(false) }
|
||||||
var createPin by remember { mutableStateOf("") }
|
var createPin by remember { mutableStateOf("") }
|
||||||
|
var clearPin by remember { mutableStateOf(false) }
|
||||||
|
var editingId by remember { mutableStateOf<String?>(null) }
|
||||||
val firstFocus = remember { FocusRequester() }
|
val firstFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
|
fun openEdit(profile: ViewerProfile) {
|
||||||
|
editingId = profile.id
|
||||||
|
pending = profile
|
||||||
|
createName = profile.name
|
||||||
|
createAvatar = profile.avatarId
|
||||||
|
createKids = profile.isKids
|
||||||
|
createPin = ""
|
||||||
|
clearPin = false
|
||||||
|
error = null
|
||||||
|
mode = ProfilesMode.Edit
|
||||||
|
}
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
@ -127,6 +142,7 @@ fun ProfilesScreen(
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 48.dp, vertical = 36.dp),
|
.padding(horizontal = 48.dp, vertical = 36.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
|
@ -139,7 +155,7 @@ fun ProfilesScreen(
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
!switcherMode && canManage -> "Voeg toe of verwijder (lang drukken)"
|
!switcherMode && canManage -> "OK = bewerken · lang drukken = verwijderen"
|
||||||
switcherMode -> "Kies je profiel"
|
switcherMode -> "Kies je profiel"
|
||||||
else -> "Alleen het hoofdprofiel mag beheren"
|
else -> "Alleen het hoofdprofiel mag beheren"
|
||||||
},
|
},
|
||||||
|
|
@ -153,50 +169,59 @@ fun ProfilesScreen(
|
||||||
if (loading) {
|
if (loading) {
|
||||||
Text("Profielen laden…", color = Color.White)
|
Text("Profielen laden…", color = Color.White)
|
||||||
} else {
|
} else {
|
||||||
LazyRow(
|
Box(
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
contentAlignment = Alignment.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
items(profiles, key = { it.id }) { p ->
|
Row(
|
||||||
ProfileTile(
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
profile = p,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = if (profiles.firstOrNull()?.id == p.id) {
|
) {
|
||||||
Modifier.focusRequester(firstFocus)
|
profiles.forEachIndexed { index, p ->
|
||||||
} else Modifier,
|
ProfileTile(
|
||||||
onClick = {
|
profile = p,
|
||||||
if (p.hasPin) {
|
modifier = if (index == 0) {
|
||||||
pending = p
|
Modifier.focusRequester(firstFocus)
|
||||||
pin = ""
|
|
||||||
mode = ProfilesMode.Pin
|
|
||||||
} else {
|
} else {
|
||||||
select(p)
|
Modifier
|
||||||
}
|
},
|
||||||
},
|
onClick = {
|
||||||
onLongDelete = if (canManage && !p.isOwner) {
|
if (!switcherMode && canManage) {
|
||||||
{
|
openEdit(p)
|
||||||
pending = p
|
} else if (p.hasPin) {
|
||||||
mode = ProfilesMode.DeleteConfirm
|
pending = p
|
||||||
}
|
pin = ""
|
||||||
} else null,
|
mode = ProfilesMode.Pin
|
||||||
)
|
} else {
|
||||||
}
|
select(p)
|
||||||
if (canManage && profiles.size < 8) {
|
}
|
||||||
item {
|
},
|
||||||
|
onLongDelete = if (!switcherMode && canManage && !p.isOwner) {
|
||||||
|
{
|
||||||
|
pending = p
|
||||||
|
mode = ProfilesMode.DeleteConfirm
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!switcherMode && canManage && profiles.size < 8) {
|
||||||
AddProfileTile(onClick = {
|
AddProfileTile(onClick = {
|
||||||
|
editingId = null
|
||||||
|
pending = null
|
||||||
createName = ""
|
createName = ""
|
||||||
createAvatar = "fox"
|
createAvatar = "fox"
|
||||||
createKids = false
|
createKids = false
|
||||||
createPin = ""
|
createPin = ""
|
||||||
|
clearPin = false
|
||||||
mode = ProfilesMode.Create
|
mode = ProfilesMode.Create
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canManage) {
|
if (!switcherMode && canManage) {
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
"Houd OK lang ingedrukt op een profiel om te verwijderen",
|
"Houd OK lang ingedrukt om te verwijderen",
|
||||||
color = Color(0xFF666666),
|
color = Color(0xFF666666),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
)
|
)
|
||||||
|
|
@ -213,38 +238,29 @@ fun ProfilesScreen(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
OutlinedTextField(
|
PinNumpad(
|
||||||
value = pin,
|
value = pin,
|
||||||
onValueChange = { v -> pin = v.filter { it.isDigit() }.take(4) },
|
onValueChange = { next ->
|
||||||
label = { androidx.compose.material3.Text("4-cijferige PIN") },
|
pin = next
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
if (next.length == 4 && target != null) select(target, next)
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
},
|
||||||
singleLine = true,
|
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedTextColor = Color.White,
|
|
||||||
unfocusedTextColor = Color.White,
|
|
||||||
focusedBorderColor = McColors.Brand,
|
|
||||||
unfocusedBorderColor = Color(0xFF555555),
|
|
||||||
focusedLabelColor = McColors.Brand,
|
|
||||||
unfocusedLabelColor = Color(0xFF888888),
|
|
||||||
cursorColor = McColors.Brand,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
ActionChip("Terug") {
|
||||||
ActionChip("Terug") {
|
mode = ProfilesMode.Pick
|
||||||
mode = ProfilesMode.Pick
|
pending = null
|
||||||
pending = null
|
pin = ""
|
||||||
pin = ""
|
|
||||||
}
|
|
||||||
ActionChip("Ontgrendel", primary = true) {
|
|
||||||
if (target != null) select(target, pin)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesMode.Create -> {
|
ProfilesMode.Create, ProfilesMode.Edit -> {
|
||||||
Text("Nieuw profiel", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.SemiBold)
|
val isEdit = mode == ProfilesMode.Edit
|
||||||
|
Text(
|
||||||
|
if (isEdit) "Profiel bewerken" else "Nieuw profiel",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = createName,
|
value = createName,
|
||||||
|
|
@ -284,52 +300,86 @@ fun ProfilesScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(14.dp))
|
if (canManage && (!isEdit || pending?.isOwner != true)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Spacer(Modifier.height(14.dp))
|
||||||
ActionChip(if (createKids) "Kids: aan" else "Kids: uit") {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
createKids = !createKids
|
ActionChip(if (createKids) "Kids: aan" else "Kids: uit") {
|
||||||
|
createKids = !createKids
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = createPin,
|
|
||||||
onValueChange = { v -> createPin = v.filter { it.isDigit() }.take(4) },
|
|
||||||
label = { androidx.compose.material3.Text("PIN (optioneel)") },
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
|
||||||
singleLine = true,
|
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedTextColor = Color.White,
|
|
||||||
unfocusedTextColor = Color.White,
|
|
||||||
focusedBorderColor = McColors.Brand,
|
|
||||||
unfocusedBorderColor = Color(0xFF555555),
|
|
||||||
focusedLabelColor = McColors.Brand,
|
|
||||||
unfocusedLabelColor = Color(0xFF888888),
|
|
||||||
cursorColor = McColors.Brand,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
ActionChip("Annuleer") { mode = ProfilesMode.Pick }
|
ActionChip("Annuleer") {
|
||||||
ActionChip("Aanmaken", primary = true) {
|
mode = ProfilesMode.Pick
|
||||||
|
editingId = null
|
||||||
|
pending = null
|
||||||
|
error = null
|
||||||
|
}
|
||||||
|
ActionChip(if (isEdit) "Opslaan" else "Aanmaken", primary = true) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
error = null
|
error = null
|
||||||
|
val editId = editingId
|
||||||
runCatching {
|
runCatching {
|
||||||
api.createProfile(
|
if (editId != null) {
|
||||||
name = createName.trim(),
|
api.updateProfile(
|
||||||
avatarId = createAvatar,
|
profileId = editId,
|
||||||
isKids = createKids,
|
name = createName.trim(),
|
||||||
pin = createPin.ifBlank { null },
|
avatarId = createAvatar,
|
||||||
)
|
isKids = if (canManage && pending?.isOwner != true) createKids else null,
|
||||||
}.onSuccess {
|
pin = createPin.ifBlank { null },
|
||||||
|
clearPin = clearPin,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
api.createProfile(
|
||||||
|
name = createName.trim(),
|
||||||
|
avatarId = createAvatar,
|
||||||
|
isKids = createKids,
|
||||||
|
pin = createPin.ifBlank { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onSuccess { saved ->
|
||||||
|
val localId = store.profileId.first()
|
||||||
|
if (localId == saved.id || editId == localId) {
|
||||||
|
store.setProfile(saved.id, saved.name, saved.avatarId)
|
||||||
|
}
|
||||||
mode = ProfilesMode.Pick
|
mode = ProfilesMode.Pick
|
||||||
|
editingId = null
|
||||||
|
pending = null
|
||||||
reload()
|
reload()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
error = it.message ?: "Aanmaken mislukt"
|
error = it.message ?: if (isEdit) "Opslaan mislukt" else "Aanmaken mislukt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(18.dp))
|
||||||
|
Text(
|
||||||
|
when {
|
||||||
|
clearPin -> "PIN wordt verwijderd bij opslaan"
|
||||||
|
isEdit && pending?.hasPin == true -> "Nieuwe PIN (optioneel)"
|
||||||
|
else -> "PIN (optioneel)"
|
||||||
|
},
|
||||||
|
color = Color(0xFFCCCCCC),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
if (isEdit && pending?.hasPin == true) {
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
ActionChip(if (clearPin) "PIN verwijderen: aan" else "PIN verwijderen") {
|
||||||
|
clearPin = !clearPin
|
||||||
|
if (clearPin) createPin = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!clearPin) {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
PinNumpad(
|
||||||
|
value = createPin,
|
||||||
|
onValueChange = { next ->
|
||||||
|
createPin = next
|
||||||
|
if (next.isNotEmpty()) clearPin = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesMode.DeleteConfirm -> {
|
ProfilesMode.DeleteConfirm -> {
|
||||||
|
|
@ -387,8 +437,8 @@ private fun ProfileTile(
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongDelete,
|
onLongClick = onLongDelete,
|
||||||
modifier = modifier.width(140.dp),
|
modifier = modifier.width(152.dp),
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(12.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(14.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.08f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.08f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = Color(0xFF1C1C24),
|
containerColor = Color(0xFF1C1C24),
|
||||||
|
|
@ -396,25 +446,34 @@ private fun ProfileTile(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.padding(16.dp),
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 18.dp, horizontal = 12.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(88.dp)
|
.size(92.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFF2E2E3A)),
|
.background(Color(0xFF2E2E3A)),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(avatarEmoji(profile.avatarId), fontSize = 40.sp)
|
Text(
|
||||||
|
avatarEmoji(profile.avatarId),
|
||||||
|
fontSize = 42.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(14.dp))
|
||||||
Text(
|
Text(
|
||||||
profile.name,
|
profile.name,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
val badge = when {
|
val badge = when {
|
||||||
profile.isKids -> "Kids"
|
profile.isKids -> "Kids"
|
||||||
|
|
@ -422,10 +481,14 @@ private fun ProfileTile(
|
||||||
profile.hasPin -> "PIN"
|
profile.hasPin -> "PIN"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (badge != null) {
|
Spacer(Modifier.height(4.dp))
|
||||||
Spacer(Modifier.height(4.dp))
|
Text(
|
||||||
Text(badge, color = Color(0xFF999999), fontSize = 12.sp)
|
badge ?: " ",
|
||||||
}
|
color = if (badge != null) Color(0xFF999999) else Color.Transparent,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -435,8 +498,8 @@ private fun ProfileTile(
|
||||||
private fun AddProfileTile(onClick: () -> Unit) {
|
private fun AddProfileTile(onClick: () -> Unit) {
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.width(140.dp),
|
modifier = Modifier.width(152.dp),
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(12.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(14.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.08f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.08f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = Color(0xFF16161E),
|
containerColor = Color(0xFF16161E),
|
||||||
|
|
@ -444,20 +507,43 @@ private fun AddProfileTile(onClick: () -> Unit) {
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.padding(16.dp),
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 18.dp, horizontal = 12.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(88.dp)
|
.size(92.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFF2A2A32)),
|
.background(Color(0xFF2A2A32)),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text("+", color = Color.White, fontSize = 36.sp, fontWeight = FontWeight.Light)
|
Text(
|
||||||
|
"+",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(14.dp))
|
||||||
Text("Toevoegen", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
Text(
|
||||||
|
"Toevoegen",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
" ",
|
||||||
|
color = Color.Transparent,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -485,3 +571,71 @@ private fun ActionChip(label: String, primary: Boolean = false, onClick: () -> U
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PinNumpad(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
maxLen: Int = 4,
|
||||||
|
) {
|
||||||
|
fun append(digit: String) {
|
||||||
|
if (value.length < maxLen) onValueChange(value + digit)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
|
repeat(maxLen) { index ->
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (index < value.length) Color.White else Color(0xFF3A3A48),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf("⌫", "0", "C"),
|
||||||
|
).forEach { row ->
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
row.forEach { key ->
|
||||||
|
Surface(
|
||||||
|
onClick = {
|
||||||
|
when (key) {
|
||||||
|
"⌫" -> if (value.isNotEmpty()) onValueChange(value.dropLast(1))
|
||||||
|
"C" -> onValueChange("")
|
||||||
|
else -> append(key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(10.dp)),
|
||||||
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.1f),
|
||||||
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
|
containerColor = Color(0xFF2A2A32),
|
||||||
|
focusedContainerColor = McColors.Brand,
|
||||||
|
contentColor = Color.White,
|
||||||
|
focusedContentColor = Color.White,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
key,
|
||||||
|
color = Color.Unspecified,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue