mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add some ui components
This commit is contained in:
parent
913334b43d
commit
d3eeb67877
12 changed files with 1169 additions and 16 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
package com.github.damontecres.dolphin.ui
|
package com.github.damontecres.dolphin.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioManager
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.compose.foundation.MarqueeAnimationMode
|
import androidx.compose.foundation.MarqueeAnimationMode
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
|
@ -7,11 +9,14 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -122,3 +127,28 @@ fun Modifier.enableMarquee(focused: Boolean) =
|
||||||
} else {
|
} else {
|
||||||
basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused)
|
basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.playSoundOnFocus(enabled: Boolean): Modifier {
|
||||||
|
if (!enabled) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
val audioManager =
|
||||||
|
remember {
|
||||||
|
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
}
|
||||||
|
return onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playOnClickSound(
|
||||||
|
context: Context,
|
||||||
|
effectType: Int = AudioManager.FX_KEY_CLICK,
|
||||||
|
) {
|
||||||
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
audioManager.playSoundEffect(effectType)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CircularProgress(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fillMaxSize: Boolean = true,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.ifElse(fillMaxSize, Modifier.fillMaxSize())) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.border,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
sealed interface DialogItemEntry
|
||||||
|
|
||||||
|
data object DialogItemDivider : DialogItemEntry
|
||||||
|
|
||||||
|
data class DialogItem(
|
||||||
|
val headlineContent: @Composable () -> Unit,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
val supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
val leadingContent: @Composable (BoxScope.() -> Unit)? = null,
|
||||||
|
val trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
) : DialogItemEntry {
|
||||||
|
constructor(
|
||||||
|
text: String,
|
||||||
|
@StringRes iconStringRes: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) : this(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
// color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = iconStringRes),
|
||||||
|
// color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) : this(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
// color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = text,
|
||||||
|
// tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) : this(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
// color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun divider(): DialogItemEntry = DialogItemDivider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DialogPopup(
|
||||||
|
showDialog: Boolean,
|
||||||
|
title: String,
|
||||||
|
dialogItems: List<DialogItemEntry>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
dismissOnClick: Boolean = true,
|
||||||
|
waitToLoad: Boolean = true,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
|
) {
|
||||||
|
var waiting by remember { mutableStateOf(waitToLoad) }
|
||||||
|
if (showDialog) {
|
||||||
|
if (waitToLoad) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// This is a hack because a long click will propagate here and click the first list item
|
||||||
|
// So this disables the list items assuming the user will stop pressing when the dialog appears
|
||||||
|
// This is also bypassed in the code below if the user releases the enter/d-pad center button
|
||||||
|
waiting = true
|
||||||
|
delay(1000)
|
||||||
|
waiting = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
waiting = false
|
||||||
|
}
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = properties,
|
||||||
|
) {
|
||||||
|
val elevatedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||||
|
LazyColumn(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
// .widthIn(min = 520.dp, max = 300.dp)
|
||||||
|
// .dialogFocusable()
|
||||||
|
.graphicsLayer {
|
||||||
|
this.clip = true
|
||||||
|
this.shape = RoundedCornerShape(28.0.dp)
|
||||||
|
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||||
|
.padding(PaddingValues(24.dp))
|
||||||
|
.onKeyEvent { event ->
|
||||||
|
val code = event.nativeKeyEvent.keyCode
|
||||||
|
if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP &&
|
||||||
|
code in
|
||||||
|
setOf(
|
||||||
|
KeyEvent.KEYCODE_ENTER,
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
waiting = false
|
||||||
|
}
|
||||||
|
false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(dialogItems) {
|
||||||
|
when (it) {
|
||||||
|
is DialogItemDivider -> HorizontalDivider(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
is DialogItem ->
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
enabled = !waiting && it.enabled,
|
||||||
|
onClick = {
|
||||||
|
if (dismissOnClick) {
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
}
|
||||||
|
it.onClick.invoke()
|
||||||
|
},
|
||||||
|
headlineContent = it.headlineContent,
|
||||||
|
overlineContent = it.overlineContent,
|
||||||
|
supportingContent = it.supportingContent,
|
||||||
|
leadingContent = it.leadingContent,
|
||||||
|
trailingContent = it.trailingContent,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScrollableDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: LazyListScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val scrollAmount = 100f
|
||||||
|
val columnState = rememberLazyListState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun scroll(reverse: Boolean = false) {
|
||||||
|
scope.launch {
|
||||||
|
columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties =
|
||||||
|
DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = columnState,
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
content = content,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.width(600.dp)
|
||||||
|
.height(380.dp)
|
||||||
|
.focusable()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
).onKeyEvent {
|
||||||
|
if (it.type == KeyEventType.KeyUp) {
|
||||||
|
return@onKeyEvent false
|
||||||
|
}
|
||||||
|
if (it.key == Key.DirectionDown) {
|
||||||
|
scroll(false)
|
||||||
|
return@onKeyEvent true
|
||||||
|
}
|
||||||
|
if (it.key == Key.DirectionUp) {
|
||||||
|
scroll(true)
|
||||||
|
return@onKeyEvent true
|
||||||
|
}
|
||||||
|
return@onKeyEvent false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MarkerDurationDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onClick: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val dialogItems =
|
||||||
|
remember {
|
||||||
|
listOf(
|
||||||
|
15.seconds,
|
||||||
|
20.seconds,
|
||||||
|
30.seconds,
|
||||||
|
60.seconds,
|
||||||
|
5.minutes,
|
||||||
|
10.minutes,
|
||||||
|
20.minutes,
|
||||||
|
).map {
|
||||||
|
DialogItem(it.toString()) {
|
||||||
|
onClick.invoke(it.inWholeMilliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = "How long?",
|
||||||
|
dialogItems = dialogItems,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
dismissOnClick = false,
|
||||||
|
waitToLoad = false,
|
||||||
|
properties = DialogProperties(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DotSeparatedRow(
|
||||||
|
texts: List<String>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
texts.forEachIndexed { index, text ->
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = textStyle,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
if (index != texts.lastIndex) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f))
|
||||||
|
.size(4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TextFieldDefaults.Container
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import com.github.damontecres.dolphin.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EditTextBox(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
height: Dp = 40.dp,
|
||||||
|
isInputValid: (String) -> Boolean = { true },
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
// From ButtonDefaults
|
||||||
|
val colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
|
unfocusedTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
|
||||||
|
errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f),
|
||||||
|
focusedIndicatorColor = MaterialTheme.colorScheme.border,
|
||||||
|
unfocusedLabelColor = Color.Unspecified,
|
||||||
|
)
|
||||||
|
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.defaultMinSize(
|
||||||
|
minWidth = TextFieldDefaults.MinWidth,
|
||||||
|
minHeight = height,
|
||||||
|
).height(height),
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge.merge(MaterialTheme.colorScheme.onPrimaryContainer),
|
||||||
|
cursorBrush = SolidColor(colors.cursorColor),
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
singleLine = true,
|
||||||
|
maxLines = 1,
|
||||||
|
minLines = 1,
|
||||||
|
visualTransformation =
|
||||||
|
if (keyboardOptions.keyboardType == KeyboardType.Password ||
|
||||||
|
keyboardOptions.keyboardType == KeyboardType.NumberPassword
|
||||||
|
) {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
} else {
|
||||||
|
VisualTransformation.None
|
||||||
|
},
|
||||||
|
decorationBox =
|
||||||
|
@Composable { innerTextField ->
|
||||||
|
// places leading icon, text field with label and placeholder, trailing icon
|
||||||
|
TextFieldDefaults.DecorationBox(
|
||||||
|
value = value,
|
||||||
|
visualTransformation =
|
||||||
|
if (keyboardOptions.keyboardType == KeyboardType.Password ||
|
||||||
|
keyboardOptions.keyboardType == KeyboardType.NumberPassword
|
||||||
|
) {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
} else {
|
||||||
|
VisualTransformation.None
|
||||||
|
},
|
||||||
|
innerTextField = innerTextField,
|
||||||
|
placeholder = placeholder,
|
||||||
|
label = null,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = null,
|
||||||
|
prefix = null,
|
||||||
|
suffix = null,
|
||||||
|
supportingText = supportingText,
|
||||||
|
shape = CircleShape,
|
||||||
|
singleLine = true,
|
||||||
|
enabled = enabled,
|
||||||
|
isError = false,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = colors,
|
||||||
|
contentPadding =
|
||||||
|
PaddingValues(
|
||||||
|
horizontal = 8.dp,
|
||||||
|
vertical = 10.dp,
|
||||||
|
),
|
||||||
|
container = {
|
||||||
|
Container(
|
||||||
|
enabled = enabled,
|
||||||
|
isError = !isInputValid.invoke(value),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = Modifier,
|
||||||
|
colors = colors,
|
||||||
|
shape = CircleShape,
|
||||||
|
focusedIndicatorLineThickness = 4.dp,
|
||||||
|
unfocusedIndicatorLineThickness = 0.dp,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchEditTextBox(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
height: Dp = 40.dp,
|
||||||
|
) {
|
||||||
|
EditTextBox(
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
modifier,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
imeAction = ImeAction.Search,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onSearch = {
|
||||||
|
onSearchClick.invoke()
|
||||||
|
this.defaultKeyboardAction(ImeAction.Done)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(R.string.search),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
readOnly,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import com.github.damontecres.dolphin.ui.AppColors
|
||||||
|
import com.github.damontecres.dolphin.ui.playOnClickSound
|
||||||
|
import com.github.damontecres.dolphin.ui.playSoundOnFocus
|
||||||
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
|
|
||||||
|
enum class StarRatingPrecision {
|
||||||
|
FULL,
|
||||||
|
HALF,
|
||||||
|
QUARTER,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromFloat(value: Float): StarRatingPrecision =
|
||||||
|
if (value <= .25f) {
|
||||||
|
QUARTER
|
||||||
|
} else if (value <= .5f) {
|
||||||
|
HALF
|
||||||
|
} else {
|
||||||
|
FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val FilledStarColor = Color(0xFFFFC700)
|
||||||
|
val EmptyStarColor = Color(0x2AFFC700)
|
||||||
|
|
||||||
|
val ratingBarHeight: Dp = 32.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StarRating(
|
||||||
|
rating100: Int,
|
||||||
|
onRatingChange: (Int) -> Unit,
|
||||||
|
precision: StarRatingPrecision,
|
||||||
|
enabled: Boolean,
|
||||||
|
playSoundOnFocus: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
bgColor: Color = AppColors.TransparentBlack75, // MaterialTheme.colorScheme.background,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var tempRating by remember(rating100) { mutableIntStateOf(rating100) }
|
||||||
|
val percentage = (if (enabled) tempRating else rating100) / 100f
|
||||||
|
val focusRequesters = remember { List(5) { FocusRequester() } }
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(bgColor),
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.selectableGroup()
|
||||||
|
.padding(4.dp)
|
||||||
|
.drawWithCache {
|
||||||
|
onDrawWithContent {
|
||||||
|
drawContent()
|
||||||
|
if (percentage in 0f..<1f) {
|
||||||
|
drawRect(
|
||||||
|
color = bgColor,
|
||||||
|
topLeft = Offset(size.width * percentage, 0f),
|
||||||
|
blendMode = BlendMode.SrcAtop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.focusGroup()
|
||||||
|
.focusProperties {
|
||||||
|
onEnter = {
|
||||||
|
val index =
|
||||||
|
if (rating100 <= 20) {
|
||||||
|
0
|
||||||
|
} else if (rating100 <= 40) {
|
||||||
|
1
|
||||||
|
} else if (rating100 <= 60) {
|
||||||
|
2
|
||||||
|
} else if (rating100 <= 80) {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
4
|
||||||
|
}
|
||||||
|
focusRequesters[index].tryRequestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
for (i in 1..5) {
|
||||||
|
item {
|
||||||
|
val isRated = (if (enabled) tempRating else rating100) >= (i * 20)
|
||||||
|
val icon = Icons.Filled.Star
|
||||||
|
var focused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val focusedColor =
|
||||||
|
if (focused) {
|
||||||
|
MaterialTheme.colorScheme.border
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(focusedColor),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
tint = FilledStarColor,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
if (enabled) {
|
||||||
|
Modifier
|
||||||
|
.onFocusChanged {
|
||||||
|
focused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
tempRating = i * 20
|
||||||
|
} else {
|
||||||
|
tempRating = rating100
|
||||||
|
}
|
||||||
|
}.playSoundOnFocus(playSoundOnFocus)
|
||||||
|
.focusRequester(focusRequesters[i - 1])
|
||||||
|
.focusProperties {
|
||||||
|
left =
|
||||||
|
if (i == 1) focusRequesters.last() else FocusRequester.Default
|
||||||
|
right =
|
||||||
|
if (i == 5) focusRequesters.first() else FocusRequester.Default
|
||||||
|
}.selectable(
|
||||||
|
selected = isRated,
|
||||||
|
onClick = {
|
||||||
|
if (playSoundOnFocus) playOnClickSound(context)
|
||||||
|
val newRating100 =
|
||||||
|
when (precision) {
|
||||||
|
StarRatingPrecision.FULL ->
|
||||||
|
if (i == 1 && rating100 > 0 && rating100 <= 20) 0 else i * 20
|
||||||
|
|
||||||
|
StarRatingPrecision.HALF -> {
|
||||||
|
if (rating100 > i * 20) {
|
||||||
|
i * 20
|
||||||
|
} else if (rating100 == i * 20) {
|
||||||
|
i * 20 - 10
|
||||||
|
} else if (rating100 == i * 20 - 10) {
|
||||||
|
(i - 1) * 20
|
||||||
|
} else if (i == 1 && rating100 == 0) {
|
||||||
|
20
|
||||||
|
} else if (i == 1 && rating100 > 10) {
|
||||||
|
10
|
||||||
|
} else if (i == 1 && rating100 <= 10) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(i) * 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StarRatingPrecision.QUARTER -> {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newRating100 != null) {
|
||||||
|
tempRating = newRating100
|
||||||
|
onRatingChange(newRating100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}.fillMaxHeight()
|
||||||
|
.aspectRatio(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Switch
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchWithLabel(
|
||||||
|
label: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onStateChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.clip(shape = RoundedCornerShape(20.dp))
|
||||||
|
.background(
|
||||||
|
if (isFocused) {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f)
|
||||||
|
},
|
||||||
|
).clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
role = Role.Switch,
|
||||||
|
onClick = {
|
||||||
|
if (enabled) {
|
||||||
|
onStateChange(!checked)
|
||||||
|
} else {
|
||||||
|
// TODO there are other uses, so shouldn't hardcode this toast
|
||||||
|
Toast
|
||||||
|
.makeText(context, "Item has no children", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = if (isFocused) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(start = 8.dp))
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
enabled = enabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
onStateChange(!checked)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TableRowComposable(
|
||||||
|
row: TableRow,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
keyWeight: Float = .3f,
|
||||||
|
valueWeight: Float = .7f,
|
||||||
|
focusable: Boolean = true,
|
||||||
|
textStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onBackground),
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val background =
|
||||||
|
if (isFocused && row.onClick != null) {
|
||||||
|
MaterialTheme.colorScheme.border.copy(alpha = .75f)
|
||||||
|
} else if (isFocused) {
|
||||||
|
MaterialTheme.colorScheme.onBackground.copy(alpha = .25f)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.background(background)
|
||||||
|
.ifElse(
|
||||||
|
row.onClick != null,
|
||||||
|
ifTrueModifier =
|
||||||
|
Modifier
|
||||||
|
.clickable(
|
||||||
|
enabled = true,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
onClick = { row.onClick?.invoke() },
|
||||||
|
),
|
||||||
|
ifFalseModifier =
|
||||||
|
Modifier.focusable(
|
||||||
|
enabled = focusable, // TODO, this allows scrolling
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val keyModifier =
|
||||||
|
Modifier
|
||||||
|
.weight(keyWeight)
|
||||||
|
val valueModifier =
|
||||||
|
Modifier
|
||||||
|
.weight(valueWeight)
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(modifier = keyModifier) {
|
||||||
|
row.key.invoke(this, Modifier.padding(4.dp))
|
||||||
|
}
|
||||||
|
Box(modifier = valueModifier) {
|
||||||
|
row.value.invoke(this, Modifier.padding(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TableRow(
|
||||||
|
val key: @Composable BoxScope.(modifier: Modifier) -> Unit,
|
||||||
|
val value: @Composable BoxScope.(modifier: Modifier) -> Unit,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
constructor(key: String, value: String, onClick: (() -> Unit)? = null) : this(
|
||||||
|
{ modifier: Modifier -> Text(text = "$key:", modifier = modifier) },
|
||||||
|
{ modifier: Modifier -> Text(text = value, modifier = modifier) },
|
||||||
|
onClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Composable
|
||||||
|
fun from(
|
||||||
|
@StringRes keyStringId: Int,
|
||||||
|
value: String?,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
): TableRow? =
|
||||||
|
if (value.isNotNullOrBlank()) {
|
||||||
|
TableRow(stringResource(keyStringId), value, onClick)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(
|
||||||
|
context: Context,
|
||||||
|
@StringRes keyStringId: Int,
|
||||||
|
value: String?,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
): TableRow? =
|
||||||
|
if (value.isNotNullOrBlank()) {
|
||||||
|
TableRow(context.getString(keyStringId), value, onClick)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(
|
||||||
|
key: String,
|
||||||
|
value: String?,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
): TableRow? =
|
||||||
|
if (value.isNotNullOrBlank()) {
|
||||||
|
TableRow(key, value, onClick)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.damontecres.dolphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
import com.github.damontecres.dolphin.ui.playOnClickSound
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitleValueText(
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
playSoundOnFocus: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
MaybeClickColumn(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
playSoundOnFocus = playSoundOnFocus,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.alpha(0.8f),
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Normal),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MaybeClickColumn(
|
||||||
|
playSoundOnFocus: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
if (onClick != null || onLongClick != null) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
if (playSoundOnFocus) {
|
||||||
|
LaunchedEffect(isFocused) {
|
||||||
|
if (isFocused) playOnClickSound(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bgColor =
|
||||||
|
if (isFocused) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = .75f)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
content = content,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.background(bgColor, shape = RoundedCornerShape(4.dp))
|
||||||
|
.ifElse(isFocused, Modifier.scale(1.1f))
|
||||||
|
.combinedClickable(
|
||||||
|
enabled = true,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
onClick = {
|
||||||
|
if (playSoundOnFocus) playOnClickSound(context)
|
||||||
|
onClick?.invoke()
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (playSoundOnFocus) playOnClickSound(context)
|
||||||
|
onLongClick?.invoke()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Column(content = content, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -12,12 +13,9 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.DateRange
|
|
||||||
import androidx.compose.material.icons.filled.Email
|
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.ShoppingCart
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
|
@ -30,7 +28,10 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
@ -45,9 +46,11 @@ import androidx.tv.material3.NavigationDrawerScope
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.rememberDrawerState
|
import androidx.tv.material3.rememberDrawerState
|
||||||
|
import com.github.damontecres.dolphin.R
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
import com.github.damontecres.dolphin.data.model.Library
|
import com.github.damontecres.dolphin.data.model.Library
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
|
@ -121,7 +124,14 @@ fun NavDrawer(
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = user?.name ?: "",
|
text = user?.name ?: "",
|
||||||
icon = Icons.Default.AccountCircle,
|
icon = Icons.Default.AccountCircle,
|
||||||
onClick = {},
|
onClick = { navigationManager.navigateTo(Destination.Setup) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
IconNavItem(
|
||||||
|
text = "Search",
|
||||||
|
icon = Icons.Default.Search,
|
||||||
|
onClick = { navigationManager.navigateTo(Destination.Search) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -148,7 +158,7 @@ fun NavDrawer(
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = "Settings",
|
text = "Settings",
|
||||||
icon = Icons.Default.Settings,
|
icon = Icons.Default.Settings,
|
||||||
onClick = {},
|
onClick = { navigationManager.navigateTo(Destination.Settings) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,20 +212,26 @@ fun NavigationDrawerScope.LibraryNavItem(
|
||||||
// TODO
|
// TODO
|
||||||
val icon =
|
val icon =
|
||||||
when (library.collectionType) {
|
when (library.collectionType) {
|
||||||
CollectionType.MOVIES -> Icons.Default.Email
|
CollectionType.MOVIES -> R.string.fa_film
|
||||||
CollectionType.TVSHOWS -> Icons.Default.DateRange
|
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||||
CollectionType.HOMEVIDEOS -> Icons.Default.ShoppingCart
|
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||||
else -> Icons.Default.Info
|
CollectionType.LIVETV -> R.string.fa_tv
|
||||||
|
CollectionType.MUSIC -> R.string.fa_music
|
||||||
|
else -> R.string.fa_film
|
||||||
}
|
}
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
icon,
|
Text(
|
||||||
contentDescription = null,
|
text = stringResource(icon),
|
||||||
)
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,6 @@
|
||||||
<string name="fa_angles_down" translatable="false"></string>
|
<string name="fa_angles_down" translatable="false"></string>
|
||||||
<string name="fa_house" translatable="false"></string>
|
<string name="fa_house" translatable="false"></string>
|
||||||
<string name="fa_closed_captioning" translatable="false"></string>
|
<string name="fa_closed_captioning" translatable="false"></string>
|
||||||
</resources>
|
<string name="fa_tv" translatable="false"></string>
|
||||||
|
<string name="fa_music" translatable="false"></string>
|
||||||
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@
|
||||||
<string name="home_section_next_up">Next up</string>
|
<string name="home_section_next_up">Next up</string>
|
||||||
<string name="home_section_livetv">Live TV</string>
|
<string name="home_section_livetv">Live TV</string>
|
||||||
<string name="home_section_none">None</string>
|
<string name="home_section_none">None</string>
|
||||||
</resources>
|
<string name="search">Search</string>
|
||||||
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue