diff --git a/composeApp/src/commonMain/composeResources/font/PoetsenOne-Regular.ttf b/composeApp/src/commonMain/composeResources/font/PoetsenOne-Regular.ttf new file mode 100644 index 0000000..1a89422 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/PoetsenOne-Regular.ttf differ diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ShinApp.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ShinApp.kt index 7b69115..e455d6c 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/ShinApp.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ShinApp.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.QrCodeScanner @@ -17,7 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.slide @@ -27,6 +29,8 @@ import `in`.procyk.compose.camera.permission.rememberCameraPermissionState import `in`.procyk.shin.component.ShinAppComponent import `in`.procyk.shin.component.ShinAppComponent.Child import `in`.procyk.shin.component.ShinAppComponent.MenuItem +import `in`.procyk.shin.ui.component.BottomBanner +import `in`.procyk.shin.ui.component.BottomBannerItem import `in`.procyk.shin.ui.component.ShinBanner import `in`.procyk.shin.ui.icons.Github import `in`.procyk.shin.ui.icons.Html5 @@ -72,10 +76,12 @@ private inline fun NavigationDrawer( val activeMenuItem by component.activeMenuItem.subscribeAsState() ModalNavigationDrawer( modifier = Modifier - .onKeyEvent { event -> - (drawerState.isOpen && event.isEscDown).also { isConsumed -> - if (isConsumed) scope.launch { drawerState.close() } + .onKeyEvent handle@{ event -> + when { + drawerState.isOpen && event.isEscDown -> scope.launch { drawerState.close() } + else -> return@handle false } + return@handle true }, drawerState = drawerState, drawerContent = { @@ -97,7 +103,10 @@ private inline fun NavigationDrawer( ).forEach { item -> NavigationDrawerItem( icon = { - Icon(item.icon, contentDescription = "Menu item icon") + Icon( + if (item == activeMenuItem) item.filledIcon else item.outlinedIcon, + contentDescription = "Menu item icon" + ) }, label = { Text(item.presentableName) @@ -114,23 +123,12 @@ private inline fun NavigationDrawer( Spacer( modifier = Modifier.weight(1f) ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("Find Me On") - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally) - ) { - FindMeOn(ShinIcons.Github, "https://github.com/avan1235/", "Github") - FindMeOn(ShinIcons.LinkedIn, "https://www.linkedin.com/in/maciej-procyk/", "LinkedIn") - FindMeOn(ShinIcons.Html5, "https://procyk.in", "web") - } - } + BottomBanner( + title = "Find Me On", + BottomBannerItem("https://github.com/avan1235/", ShinIcons.Github), + BottomBannerItem("https://www.linkedin.com/in/maciej-procyk/", ShinIcons.LinkedIn), + BottomBannerItem("https://procyk.in", ShinIcons.Html5), + ) } }, ) { @@ -180,25 +178,20 @@ private inline fun NavigationDrawer( } } -@Composable -private inline fun FindMeOn( - icon: ImageVector, - url: String, - name: String, -) { - val uriHandler = LocalUriHandler.current - IconButton(onClick = { uriHandler.openUri(url) }) { - Icon(icon, "Find me on $name") - } -} - -private inline val MenuItem.icon: ImageVector +private inline val MenuItem.outlinedIcon: ImageVector get() = when (this) { MenuItem.Main -> Icons.Outlined.Home MenuItem.ScanQRCode -> Icons.Outlined.QrCodeScanner MenuItem.Favourites -> Icons.Outlined.Favorite } +private inline val MenuItem.filledIcon: ImageVector + get() = when (this) { + MenuItem.Main -> Icons.Filled.Home + MenuItem.ScanQRCode -> Icons.Filled.QrCodeScanner + MenuItem.Favourites -> Icons.Filled.Favorite + } + private inline val MenuItem.presentableName: String get() = when (this) { MenuItem.Main -> "Home" diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/component/ShinAppComponent.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/component/ShinAppComponent.kt index 26372fb..d88f45f 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/component/ShinAppComponent.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/component/ShinAppComponent.kt @@ -14,8 +14,6 @@ interface ShinAppComponent : Component { val activeMenuItem: Value - fun onBackClicked(toIndex: Int) - fun navigateTo(item: MenuItem) enum class MenuItem { @@ -83,11 +81,6 @@ class ShinAppComponentImpl( ) } - - override fun onBackClicked(toIndex: Int) { - navigation.popTo(index = toIndex) - } - override fun navigateTo(item: MenuItem) { when (item) { MenuItem.Main -> navigation.popTo(index = 0) diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/BottomBanner.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/BottomBanner.kt new file mode 100644 index 0000000..8c25890 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/BottomBanner.kt @@ -0,0 +1,44 @@ +package `in`.procyk.shin.ui.component + +import androidx.compose.foundation.layout.* +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.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp + +data class BottomBannerItem( + val url: String, + val icon: ImageVector, +) { +} + +@Composable +internal fun BottomBanner( + title: String?, + vararg items: BottomBannerItem, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + title?.let { Text(it) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally) + ) { + val uriHandler = LocalUriHandler.current + for (item in items) { + ShinIconButton( + onClick = { uriHandler.openUri(item.url) }, + icon = item.icon, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinBanner.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinBanner.kt index df4bf89..f379a33 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinBanner.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinBanner.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.Font import shin.composeapp.generated.resources.Mansalva_Regular +import shin.composeapp.generated.resources.PoetsenOne_Regular import shin.composeapp.generated.resources.Res @Composable @@ -33,6 +34,8 @@ internal fun ShinBanner( ) Text( text = "Shorten Your URL with Kotlin", + fontFamily = FontFamily(Font(Res.font.PoetsenOne_Regular)), + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinIconButton.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinIconButton.kt new file mode 100644 index 0000000..b8823ff --- /dev/null +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/component/ShinIconButton.kt @@ -0,0 +1,53 @@ +package `in`.procyk.shin.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +internal fun ShinIconButton( + onClick: () -> Unit, + icon: ImageVector, + iconOutlined: ImageVector = icon, + contentDescription: String? = null, + size: Dp = 24.dp, + hoveredColor: Color = LocalContentColor.current, + notHoveredColor: Color = hoveredColor.copy(alpha = 0.6f), +) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(12.dp) + .clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null, + ) + ) { + Icon( + imageVector = if (isHovered) icon else iconOutlined, + contentDescription = contentDescription, + modifier = Modifier.size(size), + tint = when { + isHovered -> hoveredColor + else -> notHoveredColor + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/FavouritesScreen.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/FavouritesScreen.kt index ca8e3cf..6a75c64 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/FavouritesScreen.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/FavouritesScreen.kt @@ -4,18 +4,26 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.subscribeAsState import `in`.procyk.shin.component.FavouritesComponent +import `in`.procyk.shin.ui.component.ShinIconButton @Composable internal fun FavouritesScreen(component: FavouritesComponent) { @@ -28,13 +36,16 @@ internal fun FavouritesScreen(component: FavouritesComponent) { Text("No Favourites") } } else { - LazyColumn { + LazyColumn( + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { items(favourites, key = { it.shortUrl }) { item -> val clipboardManager = LocalClipboardManager.current Card( shape = MaterialTheme.shapes.small, modifier = Modifier - .padding(top = 16.dp, start = 16.dp, end = 16.dp) + .clip(RoundedCornerShape(12.dp)) .clickable { component.onFavouriteClick(clipboardManager, item.shortUrl) } ) { Row( @@ -44,21 +55,35 @@ internal fun FavouritesScreen(component: FavouritesComponent) { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - IconButton( - onClick = { component.removeFavourite(item.shortUrl) } + ShinIconButton( + onClick = { component.removeFavourite(item.shortUrl) }, + icon = Icons.Filled.Delete, + iconOutlined = Icons.Outlined.Delete, + contentDescription = "Delete favourite", + hoveredColor = MaterialTheme.colorScheme.error.copy(alpha = 0.5f), + notHoveredColor = LocalContentColor.current, + ) + Row( + modifier = Modifier.fillMaxWidth() ) { - Icon(Icons.Outlined.Delete, contentDescription = "Delete favourite") + Text( + text = item.fullUrl, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(2f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = item.shortUrl, + style = MaterialTheme.typography.bodyMedium.let { + it.copy(color = it.color.copy(alpha = 0.3f)) + }, + textAlign = TextAlign.End, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } - Text( - text = item.fullUrl, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - Text( - text = item.shortUrl, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.End - ) } } } diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/MainScreen.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/MainScreen.kt index e8e2d7d..69aa7c5 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/screen/MainScreen.kt @@ -9,9 +9,7 @@ import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp import `in`.procyk.shin.component.MainComponent import `in`.procyk.shin.shared.applyIf -import `in`.procyk.shin.ui.component.ShinBanner -import `in`.procyk.shin.ui.component.ShortenRequestItems -import `in`.procyk.shin.ui.component.ShortenResponse +import `in`.procyk.shin.ui.component.* import `in`.procyk.shin.ui.util.isEscDown @Composable diff --git a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/util/KeyUtil.kt b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/util/KeyUtil.kt index f2dbf6d..3a99cbb 100644 --- a/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/util/KeyUtil.kt +++ b/composeApp/src/commonMain/kotlin/in/procyk/shin/ui/util/KeyUtil.kt @@ -3,4 +3,7 @@ package `in`.procyk.shin.ui.util import androidx.compose.ui.input.key.* internal val KeyEvent.isEscDown: Boolean - get() = key == Key.Escape && type == KeyEventType.KeyDown + get() = this.key == Key.Escape && this.type == KeyEventType.KeyDown + +internal fun KeyEvent.isKeyDown(key: Key): Boolean = + this.key == key && this.type == KeyEventType.KeyDown