diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/ReferenceDevices.kt b/ui/common/src/main/kotlin/photos/network/ui/common/ReferenceDevices.kt new file mode 100644 index 00000000..0167b869 --- /dev/null +++ b/ui/common/src/main/kotlin/photos/network/ui/common/ReferenceDevices.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * 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 photos.network.ui.common + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview + +@Preview(showBackground = true, name = "phone • light", device = Devices.PHONE, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(showBackground = true, name = "phone • dark", device = Devices.PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, name = "foldable", device = Devices.FOLDABLE) +@Preview(showBackground = true, name = "tablet", device = "spec:width=1280dp,height=800dp,dpi=480") +// @Preview(showBackground = true, name = "desktop", device = "spec:width=1920dp,height=1080dp,dpi=480") +annotation class ReferenceDevices diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt index 49edbf91..a8e51ec7 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt @@ -15,13 +15,8 @@ */ package photos.network.ui.settings -import android.content.res.Configuration import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -40,8 +35,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -55,14 +48,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp @@ -70,9 +59,11 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import org.koin.androidx.compose.getViewModel import photos.network.api.ServerStatus -import photos.network.ui.common.components.AppLogo +import photos.network.ui.common.ReferenceDevices import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme +import photos.network.ui.settings.composable.ServerSetupItem +import photos.network.ui.settings.composable.SettingsHeader /** * stateful @@ -110,6 +101,7 @@ fun SettingsScreen( Column( modifier = modifier + .background(MaterialTheme.colorScheme.background) .fillMaxSize() .verticalScroll(verticalScrollState), ) { @@ -118,6 +110,7 @@ fun SettingsScreen( Text( modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(id = R.string.settings_features_pre), + color = MaterialTheme.colorScheme.onBackground, ) val map = listOf( stringResource(id = R.string.feature_sharing_title) to stringResource(id = R.string.feature_sharing_description), @@ -137,17 +130,19 @@ fun SettingsScreen( modifier = Modifier .padding(start = 8.dp, end = 8.dp) .size(8.dp) - .background(Color.Black, shape = CircleShape), + .background(MaterialTheme.colorScheme.onBackground, shape = CircleShape), ) Text( text = it.first, style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, ) } Text( modifier = Modifier.padding(start = 24.dp), text = it.second, + color = MaterialTheme.colorScheme.onBackground, ) } } @@ -157,6 +152,7 @@ fun SettingsScreen( .padding(horizontal = 16.dp) .padding(bottom = 16.dp), text = stringResource(id = R.string.settings_features_post), + color = MaterialTheme.colorScheme.onBackground, ) Divider() @@ -196,143 +192,6 @@ fun SettingsScreen( } } -@Suppress("MagicNumber") -@Composable -internal fun SettingsHeader( - modifier: Modifier = Modifier, - serverStatus: ServerStatus, -) { - // header + icon - Box( - modifier = modifier.background(MaterialTheme.colorScheme.surface), - ) { - // header gradient - Box( - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .background(MaterialTheme.colorScheme.primary) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color(0x55000000), - Color(0x00000000), - ), - ), - ), - ) - - // app name - Text( - modifier = Modifier - .padding(top = 32.dp) - .testTag("SETTINGS_HEADER_TITLE") - .fillMaxWidth(), - text = stringResource(id = R.string.app_name_full), - style = MaterialTheme.typography.headlineLarge, - textAlign = TextAlign.Center, - color = Color.White, - ) - - // logo with status indicator - AppLogo( - modifier = Modifier - .padding(top = 125.dp) - .testTag("SETTINGS_HEADER_LOGO") - .fillMaxWidth() - .align(Alignment.Center), - size = 150.dp, - serverStatus = serverStatus, - ) - } -} - -@Composable -fun ServerSetupItem( - modifier: Modifier = Modifier, - isExpanded: Boolean = false, - serverHost: String = "", - onServerHostUpdated: (String) -> Unit = {}, - isHostVerified: Boolean = false, - clientId: String = "", - onClientIdUpdated: (String) -> Unit = {}, - isClientIdVerified: Boolean = false, - serverStatus: ServerStatus = ServerStatus.UNAVAILABLE, - onServerSetupClicked: () -> Unit = {}, -) { - val serverSetupLabel = if (serverStatus != ServerStatus.AVAILABLE) { - stringResource(id = R.string.settings_item_server_setup) - } else { - stringResource(id = R.string.settings_item_server_update) - } - Surface( - modifier = modifier - .clickable( - onClickLabel = serverSetupLabel, - ) { - onServerSetupClicked() - }, - ) { - Row( - modifier = Modifier - .padding(16.dp), - ) { - Text( - modifier = Modifier.weight(1f), - text = serverSetupLabel, - ) - if (isExpanded) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - ) - } else { - Icon( - imageVector = Icons.Default.KeyboardArrowRight, - contentDescription = null, - ) - } - } - } - - Column { - // server host - AnimatedVisibility( - visible = isExpanded, - enter = fadeIn(animationSpec = tween(durationMillis = 1000)) + expandVertically(), - exit = shrinkVertically(animationSpec = tween(durationMillis = 500, delayMillis = 0)), - ) { - FormInput( - modifier = modifier, - label = "Host", - value = serverHost, - hint = "https://", - onValueChanged = { - onServerHostUpdated(it) - }, - showTrailingIcon = isHostVerified, - ) - } - - // client id - AnimatedVisibility( - visible = isExpanded && isHostVerified, - enter = fadeIn(animationSpec = tween(durationMillis = 1000)) + expandVertically(), - exit = shrinkVertically(animationSpec = tween(durationMillis = 500, delayMillis = 0)), - ) { - FormInput( - modifier = modifier, - label = "Client ID", - value = clientId, - onValueChanged = { - onClientIdUpdated(it) - }, - showTrailingIcon = isClientIdVerified, - ) - } - } -} - @Composable fun FormInput( modifier: Modifier = Modifier, @@ -456,20 +315,9 @@ fun AccountSetupItem( } } -@Preview( - "Account", - showSystemUi = true, - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO, -) -@Preview( - "Account • Dark", - showSystemUi = true, - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES, -) +@ReferenceDevices @Composable -private fun PreviewAccount( +private fun Settings( @PreviewParameter(PreviewAccountProvider::class) uiState: SettingsUiState, ) { AppTheme { diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/ServerSetupItem.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/ServerSetupItem.kt new file mode 100644 index 00000000..1b053329 --- /dev/null +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/ServerSetupItem.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * 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 photos.network.ui.settings.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import photos.network.api.ServerStatus +import photos.network.ui.settings.FormInput +import photos.network.ui.settings.R + +@Composable +fun ServerSetupItem( + modifier: Modifier = Modifier, + isExpanded: Boolean = false, + serverHost: String = "", + onServerHostUpdated: (String) -> Unit = {}, + isHostVerified: Boolean = false, + clientId: String = "", + onClientIdUpdated: (String) -> Unit = {}, + isClientIdVerified: Boolean = false, + serverStatus: ServerStatus = ServerStatus.UNAVAILABLE, + onServerSetupClicked: () -> Unit = {}, +) { + val serverSetupLabel = if (serverStatus != ServerStatus.AVAILABLE) { + stringResource(id = R.string.settings_item_server_setup) + } else { + stringResource(id = R.string.settings_item_server_update) + } + Surface( + modifier = modifier + .clickable( + onClickLabel = serverSetupLabel, + ) { + onServerSetupClicked() + }, + ) { + Row( + modifier = Modifier + .padding(16.dp), + ) { + Text( + modifier = Modifier.weight(1f), + text = serverSetupLabel, + ) + if (isExpanded) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + ) + } else { + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + contentDescription = null, + ) + } + } + } + + Column { + // server host + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn(animationSpec = tween(durationMillis = 1000)) + expandVertically(), + exit = shrinkVertically(animationSpec = tween(durationMillis = 500, delayMillis = 0)), + ) { + FormInput( + modifier = modifier, + label = "Host", + value = serverHost, + hint = "https://", + onValueChanged = { + onServerHostUpdated(it) + }, + showTrailingIcon = isHostVerified, + ) + } + + // client id + AnimatedVisibility( + visible = isExpanded && isHostVerified, + enter = fadeIn(animationSpec = tween(durationMillis = 1000)) + expandVertically(), + exit = shrinkVertically(animationSpec = tween(durationMillis = 500, delayMillis = 0)), + ) { + FormInput( + modifier = modifier, + label = "Client ID", + value = clientId, + onValueChanged = { + onClientIdUpdated(it) + }, + showTrailingIcon = isClientIdVerified, + ) + } + } +} diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/SettingsHeader.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/SettingsHeader.kt new file mode 100644 index 00000000..8db046c6 --- /dev/null +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/composable/SettingsHeader.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * 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 photos.network.ui.settings.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import photos.network.api.ServerStatus +import photos.network.ui.common.ReferenceDevices +import photos.network.ui.common.components.AppLogo +import photos.network.ui.common.theme.AppTheme +import photos.network.ui.settings.R + +@Suppress("MagicNumber") +@Composable +internal fun SettingsHeader( + modifier: Modifier = Modifier, + serverStatus: ServerStatus, +) { + // header + icon + Box( + modifier = modifier.background(MaterialTheme.colorScheme.surface), + ) { + // header gradient + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + .background(MaterialTheme.colorScheme.primary) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0x55000000), + Color(0x00000000), + ), + ), + ), + ) + + // app name + Text( + modifier = Modifier + .padding(top = 24.dp) + .testTag("SETTINGS_HEADER_TITLE") + .fillMaxWidth(), + text = stringResource(id = R.string.app_name_full), + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center, + color = Color.White, + ) + + // logo with status indicator + AppLogo( + modifier = Modifier + .padding(top = 90.dp) + .testTag("SETTINGS_HEADER_LOGO") + .fillMaxWidth() + .align(Alignment.Center), + size = 150.dp, + serverStatus = serverStatus, + ) + } +} + +@ReferenceDevices +@Composable +private fun Header( + @PreviewParameter(ServerStatusProvider::class) status: ServerStatus, +) { + AppTheme { + SettingsHeader(serverStatus = status) + } +} +private class ServerStatusProvider : PreviewParameterProvider { + override val values = sequenceOf( + ServerStatus.AVAILABLE, + ServerStatus.PROGRESS, + ServerStatus.UNAVAILABLE, + ServerStatus.UNAUTHORIZED, + ) + override val count: Int = values.count() +}