diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 045cd324..edeafca6 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -21,16 +21,13 @@ kotlin { api(compose.runtime) api(compose.foundation) api(compose.material) + api(projects.database) + implementation("com.alialbaali.kamel:kamel-image:0.2.1") } } val commonTest by getting { dependencies { implementation(kotlin("test")) } } - val androidMain by getting { - dependencies { - api("androidx.appcompat:appcompat:1.4.0-alpha02") - api("androidx.core:core-ktx:1.6.0-beta02") - } - } - val androidTest by getting { dependencies { implementation("junit:junit:4.13.2") } } + val androidMain by getting + val androidTest by getting val desktopMain by getting val desktopTest by getting } diff --git a/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Flow.kt b/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Flow.kt new file mode 100644 index 00000000..8c2e842e --- /dev/null +++ b/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Flow.kt @@ -0,0 +1,306 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.google.accompanist.flowlayout + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlin.math.max + +/** + * A composable that places its children in a horizontal flow. Unlike [Row], if the horizontal space + * is too small to put all the children in one row, multiple rows may be used. + * + * Note that just like [Row], flex values cannot be used with [FlowRow]. + * + * @param modifier The modifier to be applied to the FlowRow. + * @param mainAxisSize The size of the layout in the main axis direction. + * @param mainAxisAlignment The alignment of each row's children in the main axis direction. + * @param mainAxisSpacing The main axis spacing between the children of each row. + * @param crossAxisAlignment The alignment of each row's children in the cross axis direction. + * @param crossAxisSpacing The cross axis spacing between the rows of the layout. + * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. + */ +@Composable +public fun FlowRow( + modifier: Modifier = Modifier, + mainAxisSize: SizeMode = SizeMode.Wrap, + mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, + mainAxisSpacing: Dp = 0.dp, + crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, + crossAxisSpacing: Dp = 0.dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, + content: @Composable () -> Unit +) { + Flow( + modifier = modifier, + orientation = LayoutOrientation.Horizontal, + mainAxisSize = mainAxisSize, + mainAxisAlignment = mainAxisAlignment, + mainAxisSpacing = mainAxisSpacing, + crossAxisAlignment = crossAxisAlignment, + crossAxisSpacing = crossAxisSpacing, + lastLineMainAxisAlignment = lastLineMainAxisAlignment, + content = content + ) +} + +/** + * A composable that places its children in a vertical flow. Unlike [Column], if the vertical space + * is too small to put all the children in one column, multiple columns may be used. + * + * Note that just like [Column], flex values cannot be used with [FlowColumn]. + * + * @param modifier The modifier to be applied to the FlowColumn. + * @param mainAxisSize The size of the layout in the main axis direction. + * @param mainAxisAlignment The alignment of each column's children in the main axis direction. + * @param mainAxisSpacing The main axis spacing between the children of each column. + * @param crossAxisAlignment The alignment of each column's children in the cross axis direction. + * @param crossAxisSpacing The cross axis spacing between the columns of the layout. + * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. + */ +@Composable +public fun FlowColumn( + modifier: Modifier = Modifier, + mainAxisSize: SizeMode = SizeMode.Wrap, + mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, + mainAxisSpacing: Dp = 0.dp, + crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, + crossAxisSpacing: Dp = 0.dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, + content: @Composable () -> Unit +) { + Flow( + modifier = modifier, + orientation = LayoutOrientation.Vertical, + mainAxisSize = mainAxisSize, + mainAxisAlignment = mainAxisAlignment, + mainAxisSpacing = mainAxisSpacing, + crossAxisAlignment = crossAxisAlignment, + crossAxisSpacing = crossAxisSpacing, + lastLineMainAxisAlignment = lastLineMainAxisAlignment, + content = content + ) +} + +/** Used to specify the alignment of a layout's children, in cross axis direction. */ +public enum class FlowCrossAxisAlignment { + /** Place children such that their center is in the middle of the cross axis. */ + Center, + /** Place children such that their start edge is aligned to the start edge of the cross axis. */ + Start, + /** Place children such that their end edge is aligned to the end edge of the cross axis. */ + End, +} + +public typealias FlowMainAxisAlignment = MainAxisAlignment + +/** Layout model that arranges its children in a horizontal or vertical flow. */ +@Composable +private fun Flow( + modifier: Modifier, + orientation: LayoutOrientation, + mainAxisSize: SizeMode, + mainAxisAlignment: FlowMainAxisAlignment, + mainAxisSpacing: Dp, + crossAxisAlignment: FlowCrossAxisAlignment, + crossAxisSpacing: Dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment, + content: @Composable () -> Unit +) { + fun Placeable.mainAxisSize() = if (orientation == LayoutOrientation.Horizontal) width else height + fun Placeable.crossAxisSize() = if (orientation == LayoutOrientation.Horizontal) height else width + + Layout(content, modifier) { measurables, outerConstraints -> + val sequences = mutableListOf>() + val crossAxisSizes = mutableListOf() + val crossAxisPositions = mutableListOf() + + var mainAxisSpace = 0 + var crossAxisSpace = 0 + + val currentSequence = mutableListOf() + var currentMainAxisSize = 0 + var currentCrossAxisSize = 0 + + val constraints = OrientationIndependentConstraints(outerConstraints, orientation) + + val childConstraints = + if (orientation == LayoutOrientation.Horizontal) { + Constraints(maxWidth = constraints.mainAxisMax) + } else { + Constraints(maxHeight = constraints.mainAxisMax) + } + + // Return whether the placeable can be added to the current sequence. + fun canAddToCurrentSequence(placeable: Placeable) = + currentSequence.isEmpty() || + currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.mainAxisSize() <= + constraints.mainAxisMax + + // Store current sequence information and start a new sequence. + fun startNewSequence() { + if (sequences.isNotEmpty()) { + crossAxisSpace += crossAxisSpacing.roundToPx() + } + sequences += currentSequence.toList() + crossAxisSizes += currentCrossAxisSize + crossAxisPositions += crossAxisSpace + + crossAxisSpace += currentCrossAxisSize + mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) + + currentSequence.clear() + currentMainAxisSize = 0 + currentCrossAxisSize = 0 + } + + for (measurable in measurables) { + // Ask the child for its preferred size. + val placeable = measurable.measure(childConstraints) + + // Start a new sequence if there is not enough space. + if (!canAddToCurrentSequence(placeable)) startNewSequence() + + // Add the child to the current sequence. + if (currentSequence.isNotEmpty()) { + currentMainAxisSize += mainAxisSpacing.roundToPx() + } + currentSequence.add(placeable) + currentMainAxisSize += placeable.mainAxisSize() + currentCrossAxisSize = max(currentCrossAxisSize, placeable.crossAxisSize()) + } + + if (currentSequence.isNotEmpty()) startNewSequence() + + val mainAxisLayoutSize = + if (constraints.mainAxisMax != Constraints.Infinity && mainAxisSize == SizeMode.Expand) { + constraints.mainAxisMax + } else { + max(mainAxisSpace, constraints.mainAxisMin) + } + val crossAxisLayoutSize = max(crossAxisSpace, constraints.crossAxisMin) + + val layoutWidth = + if (orientation == LayoutOrientation.Horizontal) { + mainAxisLayoutSize + } else { + crossAxisLayoutSize + } + val layoutHeight = + if (orientation == LayoutOrientation.Horizontal) { + crossAxisLayoutSize + } else { + mainAxisLayoutSize + } + + layout(layoutWidth, layoutHeight) { + sequences.forEachIndexed { i, placeables -> + val childrenMainAxisSizes = + IntArray(placeables.size) { j -> + placeables[j].mainAxisSize() + + if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + } + val arrangement = + if (i < sequences.lastIndex) { + mainAxisAlignment.arrangement + } else { + lastLineMainAxisAlignment.arrangement + } + // TODO(soboleva): rtl support + // Handle vertical direction + val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } + with(arrangement) { arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) } + placeables.forEachIndexed { j, placeable -> + val crossAxis = + when (crossAxisAlignment) { + FlowCrossAxisAlignment.Start -> 0 + FlowCrossAxisAlignment.End -> crossAxisSizes[i] - placeable.crossAxisSize() + FlowCrossAxisAlignment.Center -> + Alignment.Center.align( + IntSize.Zero, + IntSize(width = 0, height = crossAxisSizes[i] - placeable.crossAxisSize()), + LayoutDirection.Ltr + ) + .y + } + if (orientation == LayoutOrientation.Horizontal) { + placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i] + crossAxis) + } else { + placeable.place(x = crossAxisPositions[i] + crossAxis, y = mainAxisPositions[j]) + } + } + } + } + } +} + +/** Used to specify how a layout chooses its own size when multiple behaviors are possible. */ +// TODO(popam): remove this when Flow is reworked +public enum class SizeMode { + /** + * Minimize the amount of free space by wrapping the children, subject to the incoming layout + * constraints. + */ + Wrap, + /** + * Maximize the amount of free space by expanding to fill the available space, subject to the + * incoming layout constraints. + */ + Expand +} + +/** Used to specify the alignment of a layout's children, in main axis direction. */ +public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { + // TODO(soboleva) support RTl in Flow + // workaround for now - use Arrangement that equals to previous Arrangement + /** Place children such that they are as close as possible to the middle of the main axis. */ + Center(Arrangement.Center), + + /** Place children such that they are as close as possible to the start of the main axis. */ + Start(Arrangement.Top), + + /** Place children such that they are as close as possible to the end of the main axis. */ + End(Arrangement.Bottom), + + /** + * Place children such that they are spaced evenly across the main axis, including free space + * before the first child and after the last child. + */ + SpaceEvenly(Arrangement.SpaceEvenly), + + /** + * Place children such that they are spaced evenly across the main axis, without free space before + * the first child or after the last child. + */ + SpaceBetween(Arrangement.SpaceBetween), + + /** + * Place children such that they are spaced evenly across the main axis, including free space + * before the first child and after the last child, but half the amount of space existing + * otherwise between two consecutive children. + */ + SpaceAround(Arrangement.SpaceAround) +} diff --git a/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Layout.kt b/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Layout.kt new file mode 100644 index 00000000..6b644e2f --- /dev/null +++ b/common/src/commonMain/kotlin/com/google/accompanist/flowlayout/Layout.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.google.accompanist.flowlayout + +import androidx.compose.ui.unit.Constraints + +internal enum class LayoutOrientation { + Horizontal, + Vertical +} + +internal data class OrientationIndependentConstraints( + val mainAxisMin: Int, + val mainAxisMax: Int, + val crossAxisMin: Int, + val crossAxisMax: Int +) { + constructor( + c: Constraints, + orientation: LayoutOrientation + ) : this( + if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight, + if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight, + if (orientation === LayoutOrientation.Horizontal) c.minHeight else c.minWidth, + if (orientation === LayoutOrientation.Horizontal) c.maxHeight else c.maxWidth + ) +} diff --git a/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/posts/LobstersItem.kt b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/posts/LobstersItem.kt new file mode 100644 index 00000000..cc2a5c34 --- /dev/null +++ b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/posts/LobstersItem.kt @@ -0,0 +1,202 @@ +@file:Suppress("FunctionName") + +package dev.msfjarvis.claw.common.posts + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.IconButton +import androidx.compose.material.IconToggleButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.google.accompanist.flowlayout.FlowRow +import dev.msfjarvis.lobsters.data.local.SavedPost +import io.kamel.image.KamelImage +import io.kamel.image.lazyImageResource + +@Composable +fun LobstersItem( + post: SavedPost, + isSaved: Boolean, + viewPost: () -> Unit, + viewComments: (String) -> Unit, + toggleSave: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = Modifier.clickable { viewPost.invoke() }.then(modifier), + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) { + PostTitle( + title = post.title, + modifier = Modifier.padding(bottom = 4.dp), + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + TagRow( + tags = post.tags, + modifier = Modifier.weight(0.65f), + ) + SaveButton( + isSaved = isSaved, + onClick = toggleSave, + ) + Spacer( + modifier = Modifier.width(8.dp), + ) + CommentsButton( + onClick = { viewComments(post.shortId) }, + ) + } + SubmitterName( + text = "Submitted by %s".format(post.submitterName), + avatarUrl = post.submitterAvatarUrl, + contentDescription = "Submitted by %s".format(post.submitterName), + ) + } + } +} + +@Composable +fun PostTitle( + title: String, + modifier: Modifier = Modifier, +) { + Text( + text = title, + color = Color.Black, + fontWeight = FontWeight.Bold, + modifier = Modifier.then(modifier), + ) +} + +@Composable +fun SubmitterName( + text: String, + avatarUrl: String, + contentDescription: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = Modifier.then(modifier), + verticalAlignment = Alignment.CenterVertically, + ) { + SubmitterAvatar( + avatarUrl = avatarUrl, + contentDescription = contentDescription, + ) + SubmitterNameText( + text = text, + ) + } +} + +@Composable +fun SubmitterAvatar( + avatarUrl: String, + contentDescription: String, +) { + KamelImage( + resource = lazyImageResource(avatarUrl), + contentDescription = contentDescription, + modifier = Modifier.requiredSize(24.dp), + crossfade = true, + ) +} + +@Composable +fun SubmitterNameText( + text: String, +) { + Text( + text = text, + modifier = Modifier.padding(start = 4.dp), + ) +} + +@Composable +fun SaveButton( + isSaved: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconToggleButton( + checked = isSaved, + onCheckedChange = { onClick.invoke() }, + modifier = Modifier.requiredSize(32.dp).then(modifier), + ) { + Crossfade(targetState = isSaved) { saved -> + /*IconResource( + resourceId = if (saved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px, + tint = MaterialTheme.colors.secondary, + contentDescription = + if (saved) Strings.RemoveFromSavedPosts.get() else Strings.AddToSavedPosts.get(), + )*/ + } + } +} + +@Composable +fun CommentsButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + onClick = onClick, + modifier = Modifier.requiredSize(32.dp).then(modifier), + ) { + /*IconResource( + resourceId = R.drawable.ic_insert_comment_24px, + tint = MaterialTheme.colors.secondary, + contentDescription = Strings.OpenComments.get(), + )*/ + } +} + +@Composable +fun TagRow( + tags: List, + modifier: Modifier = Modifier, +) { + Box( + modifier = Modifier.then(modifier), + ) { + FlowRow( + mainAxisSpacing = 8.dp, + crossAxisSpacing = 8.dp, + ) { + tags.forEach { tag -> + Text( + text = tag, + modifier = + Modifier.background( + MaterialTheme.colors.secondary.copy(alpha = 0.75f), + RoundedCornerShape(8.dp) + ) + .padding(vertical = 2.dp, horizontal = 6.dp), + color = MaterialTheme.colors.onSecondary, + ) + } + } + } +}