From a3cb47e87245a102f993fbc1f02e1cd8be43db92 Mon Sep 17 00:00:00 2001 From: Daniel Yrovas Date: Mon, 19 Feb 2024 12:28:04 +1100 Subject: [PATCH] add info to preference screen --- .../ui/common/PreferenceComponents.kt | 161 ++++++++++++++++-- .../linklater/ui/screens/PreferencesScreen.kt | 76 ++++++++- .../main/java/org/yrovas/linklater/utils.kt | 11 ++ gradle/libs.versions.toml | 4 +- 4 files changed, 231 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/yrovas/linklater/ui/common/PreferenceComponents.kt b/app/src/main/java/org/yrovas/linklater/ui/common/PreferenceComponents.kt index 801292b..a72b6fd 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/common/PreferenceComponents.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/common/PreferenceComponents.kt @@ -1,20 +1,28 @@ package org.yrovas.linklater.ui.common +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import org.yrovas.linklater.readClipboard import org.yrovas.linklater.ui.screens.ThemePreview import org.yrovas.linklater.ui.theme.AppTheme import org.yrovas.linklater.ui.theme.padding @@ -24,7 +32,9 @@ fun TextPreference( icon: ImageVector, name: String, placeholder: String = "", - secure: Boolean = true, + infoPreview: String = "", + infoTitle: String = "", + info: (@Composable () -> Unit)? = null, state: State, onSave: (String) -> Unit, onCheck: (String) -> Boolean, @@ -33,7 +43,9 @@ fun TextPreference( icon = { Icon(icon) }, name = name, placeholder = placeholder, - secure = secure, + infoPreview = infoPreview, + infoTitle = infoTitle, + info = info, state = state, onSave = onSave, onCheck = onCheck, @@ -44,20 +56,31 @@ fun TextPreference( private fun TextPreference( icon: @Composable () -> Unit = {}, name: String, + infoPreview: String, + infoTitle: String, + info: (@Composable () -> Unit)?, placeholder: String, - secure: Boolean, state: State, onSave: (String) -> Unit, onCheck: (String) -> Boolean, ) { var showDialog by remember { mutableStateOf(false) } + if (showDialog) { Dialog(properties = DialogProperties( dismissOnBackPress = true, dismissOnClickOutside = true, - securePolicy = if (secure) SecureFlagPolicy.SecureOn else SecureFlagPolicy.SecureOff, ), onDismissRequest = { showDialog = false }) { - TextEditDialog(name, placeholder, state, onSave, onCheck) { + TextEditDialog( + name, + placeholder, + infoPreview, + infoTitle, + info, + state, + onSave, + onCheck + ) { showDialog = false } } @@ -72,7 +95,8 @@ private fun TextPreference( Column { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth() ) { icon() Spacer(modifier = Modifier.width(padding.standard)) @@ -101,13 +125,18 @@ private fun TextPreference( private fun TextEditDialog( name: String, placeholder: String, + infoPreview: String, + infoTitle: String, + info: (@Composable () -> Unit)?, storedValue: State, onSave: (String) -> Unit, onCheck: (String) -> Boolean, onDismiss: () -> Unit, ) { + val context: Context = LocalContext.current var currentInput by remember { mutableStateOf(TextFieldValue(storedValue.value)) } var isValid by remember { mutableStateOf(onCheck(storedValue.value)) } + var showInfo by remember { mutableStateOf(false) } Surface( color = MaterialTheme.colorScheme.surfaceContainer, @@ -116,7 +145,9 @@ private fun TextEditDialog( Column( modifier = Modifier .wrapContentHeight() - .padding(vertical = padding.standard, horizontal = padding.standard) + .padding( + vertical = padding.standard, horizontal = padding.standard + ) .fillMaxWidth() ) { Text( @@ -136,8 +167,59 @@ private fun TextEditDialog( currentInput = it }) Spacer(modifier = Modifier.height(padding.standard)) + + if (info != null) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(5.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth() + .clickable { showInfo = !showInfo } + .padding(padding.standard) + ) { + Crossfade( + label = "Info Title", targetState = showInfo + ) { + if (it) Text(text = infoTitle, color = MaterialTheme.colorScheme.onSecondaryContainer) + else Text(text = infoPreview, color = MaterialTheme.colorScheme.onSecondaryContainer) + } + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = if (showInfo) Icons.Default.Info else Icons.Outlined.Info + ) + } + AnimatedVisibility( + modifier = Modifier.padding( + start = padding.standard, + end = padding.standard, + bottom = padding.standard, + ), visible = showInfo + ) { + info() + } + } + Spacer(modifier = Modifier.height(padding.standard)) + } + Row { Spacer(modifier = Modifier.weight(1f)) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + onClick = { + currentInput = TextFieldValue(readClipboard(context)) + }, + ) { + Icon(imageVector = Icons.Default.ContentPasteGo) + } + Spacer(modifier = Modifier.width(padding.standard)) Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -154,9 +236,31 @@ private fun TextEditDialog( } } +@ThemePreview +@Composable +private fun TextPreferenceURLPreview() { + val state = remember { mutableStateOf("") } + AppTheme { + Surface { + Row( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(padding.standard) + ) { + TextPreference(icon = Icons.Default.Build, + name = "LinkDing API URL", + state = state, + onSave = {}, + onCheck = { true }) + } + } + } +} + @ThemePreview @Composable private fun TextPreferencePreview() { + val state = remember { mutableStateOf("APITokenstringVeryLongAndRandom") } AppTheme { Surface { Row( @@ -166,7 +270,7 @@ private fun TextPreferencePreview() { ) { TextPreference(icon = Icons.Default.Build, name = "LinkDing API Token", - state = derivedStateOf { "APITokenstringVeryLongAndRandom" }, + state = state, onSave = {}, onCheck = { true }) } @@ -177,6 +281,7 @@ private fun TextPreferencePreview() { @ThemePreview @Composable private fun TextEditDialogPreview() { + val state = remember { mutableStateOf("") } AppTheme { Surface { Row( @@ -184,9 +289,39 @@ private fun TextEditDialogPreview() { .background(MaterialTheme.colorScheme.background) .padding(padding.double) ) { - TextEditDialog(name = "LinkDing API Token", - placeholder = "Enter your API token...", - storedValue = derivedStateOf { "" }, + TextEditDialog(name = "LinkDing API URL", + placeholder = "Enter your LinkDing instance URL...", + infoPreview = "include /api", + infoTitle = "Enter the LinkDing API URL", + // the protocol (https://), and the path (`/api`). e.g. `https://demo.linkding.link/api`", + info = { + Column { + Text("Include the protocol (https://).", color = MaterialTheme.colorScheme.onSecondaryContainer) + Text("Include the /api path.", color = MaterialTheme.colorScheme.onSecondaryContainer) + Text("Include the port if necessary.", color = MaterialTheme.colorScheme.onSecondaryContainer) + Text("For example", color = MaterialTheme.colorScheme.onSecondaryContainer) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(padding.half) + ) { + Text("https://demo.linkding.link/api", color = MaterialTheme.colorScheme.onSurface) + } + Text("or", color = MaterialTheme.colorScheme.onSecondaryContainer) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(padding.half) + ) { + Text("http://192.168.0.47:8000/api", color = MaterialTheme.colorScheme.onSurface) + } + } + }, + storedValue = state, onSave = {}, onCheck = { true }) {} } diff --git a/app/src/main/java/org/yrovas/linklater/ui/screens/PreferencesScreen.kt b/app/src/main/java/org/yrovas/linklater/ui/screens/PreferencesScreen.kt index 520ffd1..bb56acf 100644 --- a/app/src/main/java/org/yrovas/linklater/ui/screens/PreferencesScreen.kt +++ b/app/src/main/java/org/yrovas/linklater/ui/screens/PreferencesScreen.kt @@ -1,18 +1,18 @@ package org.yrovas.linklater.ui.screens import android.content.Context +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Create -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator @@ -47,6 +47,56 @@ fun PreferencesScreen( name = "LinkDing URL", placeholder = "URL/IP incl. port and https://", icon = Icons.Default.Create, + infoPreview = "include /api", + infoTitle = "Enter the LinkDing API URL", + info = { + Column { + Text( + "Include the protocol (https://).", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + "Include the /api path.", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + "Include the port if necessary.", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + "For example", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(padding.half) + ) { + Text( + "https://demo.linkding.link/api", + color = MaterialTheme.colorScheme.onSurface + ) + } + Text( + "or", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(padding.half) + ) { + Text( + "http://192.168.0.47:8000/api", + color = MaterialTheme.colorScheme.onSurface + ) + } + } + }, state = mainActivityState.bookmarkURL.collectAsState(), onSave = { scope.launch { @@ -57,8 +107,22 @@ fun PreferencesScreen( ) TextPreference( name = "LinkDing API Token", - placeholder = "Enter your LinkDing API Token", + placeholder = "Enter your REST API Token", icon = Icons.Default.Build, + infoPreview = "Settings > Integrations", + infoTitle = "Go to your Instance Settings", + info = { + Column { + Text( + "Select Integrations", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + "Copy the token under REST API.", + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + }, state = mainActivityState.bookmarkAPIToken.collectAsState(), onSave = { scope.launch { diff --git a/app/src/main/java/org/yrovas/linklater/utils.kt b/app/src/main/java/org/yrovas/linklater/utils.kt index 50741f1..8bda39e 100644 --- a/app/src/main/java/org/yrovas/linklater/utils.kt +++ b/app/src/main/java/org/yrovas/linklater/utils.kt @@ -36,3 +36,14 @@ fun openBrowser(context: Context, uri: Uri) { val browserIntent = Intent(Intent.ACTION_VIEW, uri) context.startActivity(browserIntent); } + +fun readClipboard(context: Context): String { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + if (clipboardManager.hasPrimaryClip()) { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + return clipData.getItemAt(0).text?.toString() ?: "" + } + } + return "" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad3b67f..10507c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] app-versionID = "org.yrovas.linklater" -app-versionCode = "2" -app-versionName = "0.1.1" +app-versionCode = "3" +app-versionName = "0.1.2" app-compileSDK = "34" app-targetSDK = "34" app-minimumSDK = "23"