diff --git a/core/designsystem/src/main/java/soup/movie/core/designsystem/tools/DevicePreviews.kt b/core/designsystem/src/main/java/soup/movie/core/designsystem/tools/DevicePreviews.kt deleted file mode 100644 index a1b5f1f0..00000000 --- a/core/designsystem/src/main/java/soup/movie/core/designsystem/tools/DevicePreviews.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 SOUP - * - * 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 soup.movie.core.designsystem.tools - -import androidx.compose.ui.tooling.preview.Preview - -/** - * Multipreview annotation that represents various device sizes. - * Add this annotation to a composable to render various devices. - */ -@Preview(name = "phone", widthDp = 360, heightDp = 640) -@Preview(name = "landscape", widthDp = 640, heightDp = 360) -@Preview(name = "foldable", device = "id:pixel_fold") -@Preview(name = "tablet", device = "id:pixel_tablet") -annotation class DevicePreviews diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/BoxOffice.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/BoxOffice.kt index f23c5a70..66e3f4b1 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/BoxOffice.kt +++ b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/BoxOffice.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import soup.movie.core.designsystem.theme.MovieTheme @@ -119,7 +119,7 @@ fun BoxOffice( } } -@Preview +@PreviewLightDark @Composable private fun BoxOfficePreview() { MovieTheme { diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeBadge.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeBadge.kt index b91015d3..f5d842c3 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeBadge.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeBadge.kt @@ -18,11 +18,13 @@ package soup.movie.feature.home.impl.favorite import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import soup.movie.core.designsystem.theme.MovieTheme @@ -45,3 +47,14 @@ fun MovieAgeBadge( .border(1.dp, MovieTheme.colors.background, shape = RoundedCornerShape(5.dp)), ) } + +@PreviewLightDark +@Composable +private fun MovieAgeBadgePreview() { + MovieTheme { + MovieAgeBadge( + age = 15, + modifier = Modifier.padding(all = 4.dp), + ) + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeTag.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeTag.kt index af2b9138..aec14248 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeTag.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieAgeTag.kt @@ -15,8 +15,11 @@ */ package soup.movie.feature.home.impl.favorite +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import soup.movie.core.designsystem.theme.MovieTheme import soup.movie.feature.home.impl.textUnit @@ -40,3 +43,16 @@ fun MovieAgeTag( fontSize = 12.dp.textUnit, ) } + +@PreviewLightDark +@Composable +private fun MovieAgeTagPreview() { + MovieTheme { + Surface { + MovieAgeTag( + age = 15, + modifier = Modifier.padding(all = 4.dp), + ) + } + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieDDayTag.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieDDayTag.kt index 51f38c78..03f41a0f 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieDDayTag.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/favorite/MovieDDayTag.kt @@ -15,8 +15,10 @@ */ package soup.movie.feature.home.impl.favorite +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import soup.movie.core.designsystem.theme.MovieTheme import soup.movie.feature.home.impl.textUnit @@ -33,3 +35,14 @@ fun MovieDDayTag( fontSize = 12.dp.textUnit, ) } + +@PreviewLightDark +@Composable +private fun MovieDDayTagPreview() { + MovieTheme { + MovieDDayTag( + text = "D-1", + modifier = Modifier.padding(all = 4.dp), + ) + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterAge.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterAge.kt new file mode 100644 index 00000000..3740d8f2 --- /dev/null +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterAge.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2024 SOUP + * + * 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 soup.movie.feature.home.impl.filter + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import soup.movie.core.designsystem.theme.MovieTheme +import soup.movie.resources.R + +@Composable +fun HomeFilterAge(viewModel: HomeFilterViewModel) { + val ageUiModel by viewModel.ageUiModel.collectAsState() + HomeFilterAge( + ageUiModel = ageUiModel, + onAgeAllFilterClicked = { viewModel.onAgeAllFilterClicked() }, + onAge12FilterClicked = { viewModel.onAge12FilterClicked() }, + onAge15FilterClicked = { viewModel.onAge15FilterClicked() }, + onAge19FilterClicked = { viewModel.onAge19FilterClicked() }, + ) +} + +@Composable +fun HomeFilterAge( + ageUiModel: AgeFilterUiModel?, + onAgeAllFilterClicked: () -> Unit, + onAge12FilterClicked: () -> Unit, + onAge15FilterClicked: () -> Unit, + onAge19FilterClicked: () -> Unit, +) { + Column(modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp)) { + HomeFilterCategory(text = stringResource(R.string.filter_category_age)) + Row( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.Center, + ) { + ageUiModel?.let { + HomeFilterAgeText( + text = "전체", + selected = it.hasAll, + modifier = Modifier + .padding(end = 2.dp) + .clickable { onAgeAllFilterClicked() } + .background( + color = if (it.hasAll) Color(0xFF4CAF50) else Color(0x664CAF50), + shape = RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp), + ), + ) + HomeFilterAgeText( + text = "12", + selected = it.has12, + modifier = Modifier + .padding(end = 2.dp) + .clickable { onAge12FilterClicked() } + .background( + color = if (it.has12) Color(0xFF2196F3) else Color(0x662196F3), + ), + ) + HomeFilterAgeText( + text = "15", + selected = it.has15, + modifier = Modifier + .padding(end = 2.dp) + .clickable { onAge15FilterClicked() } + .background( + color = if (it.has15) Color(0xFFFFC107) else Color(0x66FFC107), + ), + ) + HomeFilterAgeText( + text = "청불", + selected = it.has19, + modifier = Modifier + .clickable { onAge19FilterClicked() } + .background( + color = if (it.has19) Color(0xFFF44336) else Color(0x66F44336), + shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp), + ), + ) + } + } + } +} + +@Composable +private fun HomeFilterAgeText( + text: String, + selected: Boolean, + modifier: Modifier = Modifier, +) { + val textColor = if (selected) { + Color.White + } else { + Color(0xAAFFFFFF) + } + Text( + text = text, + color = textColor, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = modifier + .requiredWidth(72.dp) + .wrapContentHeight() + .padding(vertical = 6.dp), + ) +} + +@PreviewLightDark +@Composable +private fun HomeFilterAgePreview() { + MovieTheme { + Surface { + Column { + listOf(true, false).forEach { + HomeFilterAge( + ageUiModel = AgeFilterUiModel( + hasAll = it, + has12 = it, + has15 = it, + has19 = it, + ), + onAgeAllFilterClicked = {}, + onAge12FilterClicked = {}, + onAge15FilterClicked = {}, + onAge19FilterClicked = {}, + ) + HorizontalDivider() + } + } + } + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterCategory.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterCategory.kt new file mode 100644 index 00000000..d6f5422d --- /dev/null +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterCategory.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 SOUP + * + * 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 soup.movie.feature.home.impl.filter + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import soup.movie.core.designsystem.theme.MovieTheme + +@Composable +fun HomeFilterCategory( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + modifier = modifier.fillMaxWidth(), + ) +} + +@PreviewLightDark +@Composable +private fun HomeFilterCategoryPreview() { + MovieTheme { + Surface { + HomeFilterCategory(text = "Category") + } + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterScreen.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterScreen.kt index 89aeca70..b088cbae 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterScreen.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterScreen.kt @@ -15,34 +15,13 @@ */ package soup.movie.feature.home.impl.filter -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import soup.movie.resources.R @Composable fun HomeFilterScreen( @@ -67,135 +46,3 @@ private fun HomeFilterDivider() { modifier = Modifier.padding(horizontal = 16.dp), ) } - -@Composable -private fun HomeFilterCategory(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleMedium, - ) -} - -@Composable -private fun HomeFilterTheater(viewModel: HomeFilterViewModel) { - val theaterUiModel by viewModel.theaterUiModel.collectAsState() - Column(modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp)) { - HomeFilterCategory(text = stringResource(R.string.filter_category_theater)) - Row( - modifier = Modifier - .padding(top = 12.dp) - .fillMaxWidth() - .wrapContentHeight(), - horizontalArrangement = Arrangement.Center, - ) { - theaterUiModel?.let { uiModel -> - CgvFilterChip( - text = "CGV", - checked = uiModel.hasCgv, - onCheckedChange = { isChecked -> - viewModel.onCgvFilterChanged(isChecked) - }, - ) - Spacer(modifier = Modifier.width(8.dp)) - LotteFilterChip( - text = "롯데시네마", - checked = uiModel.hasLotteCinema, - onCheckedChange = { isChecked -> - viewModel.onLotteFilterChanged(isChecked) - }, - ) - Spacer(modifier = Modifier.width(8.dp)) - MegaboxFilterChip( - text = "메가박스", - checked = uiModel.hasMegabox, - onCheckedChange = { isChecked -> - viewModel.onMegaboxFilterChanged(isChecked) - }, - ) - } - } - } -} - -@Composable -private fun HomeFilterAge(viewModel: HomeFilterViewModel) { - val ageUiModel by viewModel.ageUiModel.collectAsState() - Column(modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp)) { - HomeFilterCategory(text = stringResource(R.string.filter_category_age)) - Row( - modifier = Modifier - .padding(top = 12.dp) - .fillMaxWidth() - .wrapContentHeight(), - horizontalArrangement = Arrangement.Center, - ) { - ageUiModel?.let { - HomeFilterAgeText( - text = "전체", - selected = it.hasAll, - modifier = Modifier - .padding(end = 2.dp) - .clickable { viewModel.onAgeAllFilterClicked() } - .background( - color = if (it.hasAll) Color(0xFF4CAF50) else Color(0x664CAF50), - shape = RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp), - ), - ) - HomeFilterAgeText( - text = "12", - selected = it.has12, - modifier = Modifier - .padding(end = 2.dp) - .clickable { viewModel.onAge12FilterClicked() } - .background( - color = if (it.has12) Color(0xFF2196F3) else Color(0x662196F3), - ), - ) - HomeFilterAgeText( - text = "15", - selected = it.has15, - modifier = Modifier - .padding(end = 2.dp) - .clickable { viewModel.onAge15FilterClicked() } - .background( - color = if (it.has15) Color(0xFFFFC107) else Color(0x66FFC107), - ), - ) - HomeFilterAgeText( - text = "청불", - selected = it.has19, - modifier = Modifier - .clickable { viewModel.onAge19FilterClicked() } - .background( - color = if (it.has19) Color(0xFFF44336) else Color(0x66F44336), - shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp), - ), - ) - } - } - } -} - -@Composable -private fun HomeFilterAgeText( - text: String, - selected: Boolean, - modifier: Modifier = Modifier, -) { - val textColor = if (selected) { - Color.White - } else { - Color(0xAAFFFFFF) - } - Text( - text = text, - color = textColor, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - modifier = modifier - .requiredWidth(72.dp) - .wrapContentHeight() - .padding(vertical = 6.dp), - ) -} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterTheater.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterTheater.kt new file mode 100644 index 00000000..f852de27 --- /dev/null +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/filter/HomeFilterTheater.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 SOUP + * + * 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 soup.movie.feature.home.impl.filter + +import androidx.compose.foundation.layout.Arrangement +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.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import soup.movie.core.designsystem.theme.MovieTheme +import soup.movie.resources.R + +@Composable +fun HomeFilterTheater(viewModel: HomeFilterViewModel) { + val theaterUiModel by viewModel.theaterUiModel.collectAsState() + HomeFilterTheater( + theaterUiModel = theaterUiModel, + onCgvFilterChanged = { viewModel.onCgvFilterChanged(it) }, + onLotteFilterChanged = { viewModel.onLotteFilterChanged(it) }, + onMegaboxFilterChanged = { viewModel.onMegaboxFilterChanged(it) }, + ) +} + +@Composable +private fun HomeFilterTheater( + theaterUiModel: TheaterFilterUiModel?, + onCgvFilterChanged: (Boolean) -> Unit, + onLotteFilterChanged: (Boolean) -> Unit, + onMegaboxFilterChanged: (Boolean) -> Unit, +) { + Column(modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp)) { + HomeFilterCategory(text = stringResource(R.string.filter_category_theater)) + Row( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.Center, + ) { + theaterUiModel?.let { uiModel -> + CgvFilterChip( + text = "CGV", + checked = uiModel.hasCgv, + onCheckedChange = { isChecked -> + onCgvFilterChanged(isChecked) + }, + ) + Spacer(modifier = Modifier.width(8.dp)) + LotteFilterChip( + text = "롯데시네마", + checked = uiModel.hasLotteCinema, + onCheckedChange = { isChecked -> + onLotteFilterChanged(isChecked) + }, + ) + Spacer(modifier = Modifier.width(8.dp)) + MegaboxFilterChip( + text = "메가박스", + checked = uiModel.hasMegabox, + onCheckedChange = { isChecked -> + onMegaboxFilterChanged(isChecked) + }, + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun HomeFilterTheaterPreview() { + MovieTheme { + HomeFilterTheater( + theaterUiModel = TheaterFilterUiModel( + hasCgv = true, + hasLotteCinema = true, + hasMegabox = true, + ), + onCgvFilterChanged = {}, + onLotteFilterChanged = {}, + onMegaboxFilterChanged = {}, + ) + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/CommonError.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/CommonError.kt new file mode 100644 index 00000000..7474f022 --- /dev/null +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/CommonError.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024 SOUP + * + * 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 soup.movie.feature.home.impl.tab + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +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.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import soup.movie.core.designsystem.icon.MovieIcons +import soup.movie.core.designsystem.theme.MovieTheme +import soup.movie.resources.R + +@Composable +fun CommonError( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .requiredHeight(40.dp), + color = MovieTheme.colors.error, + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + MovieIcons.Info, + contentDescription = null, + tint = MovieTheme.colors.onError, + ) + Text( + text = stringResource(R.string.common_network_error), + modifier = Modifier.padding(start = 20.dp, end = 16.dp), + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MovieTheme.typography.bodySmall, + ) + } + } +} + +@PreviewLightDark +@Composable +private fun CommonErrorPreview() { + MovieTheme { + Surface { + CommonError(onClick = {}) + } + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/ContentLoadingProgressBar.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/ContentLoadingProgressBar.kt index a52ba1ba..acc50028 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/ContentLoadingProgressBar.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/ContentLoadingProgressBar.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface @@ -37,6 +38,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import soup.movie.core.designsystem.icon.MovieIcons @@ -51,7 +53,7 @@ fun ContentLoadingProgressBar( elevation: Dp = 12.dp, ) { Surface( - modifier = modifier, + modifier = modifier.requiredSize(size = 48.dp), shape = shape, color = backgroundColor, contentColor = contentColor, @@ -61,7 +63,7 @@ fun ContentLoadingProgressBar( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - val transition = rememberInfiniteTransition() + val transition = rememberInfiniteTransition(label = "ContentLoadingProgressBar") val currentRotation by transition.animateFloat( initialValue = 0f, targetValue = 1f, @@ -71,6 +73,7 @@ fun ContentLoadingProgressBar( easing = LinearEasing, ), ), + label = "currentRotation", ) Box( modifier = Modifier @@ -99,3 +102,15 @@ fun ContentLoadingProgressBar( } } } + +@PreviewLightDark +@Composable +private fun ContentLoadingProgressBarPreview() { + MovieTheme { + Surface { + ContentLoadingProgressBar( + modifier = Modifier.padding(16.dp), + ) + } + } +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/HomeContentsScreen.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/HomeContentsScreen.kt index 30aa0369..188d66c7 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/HomeContentsScreen.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/HomeContentsScreen.kt @@ -19,32 +19,17 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -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.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import soup.movie.core.designsystem.icon.MovieIcons -import soup.movie.core.designsystem.theme.MovieTheme import soup.movie.model.MovieModel -import soup.movie.resources.R @Composable fun HomeContentsScreen( @@ -71,10 +56,7 @@ fun HomeContentsScreen( if (isError) { CommonError( onClick = onErrorClick, - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), ) } AnimatedVisibility( @@ -97,36 +79,3 @@ fun HomeContentsScreen( } } } - -@Composable -private fun CommonError( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Surface( - onClick = onClick, - modifier = modifier, - color = MovieTheme.colors.error, - ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - MovieIcons.Info, - contentDescription = null, - tint = MovieTheme.colors.onError, - ) - Text( - text = stringResource(R.string.common_network_error), - modifier = Modifier.padding(start = 20.dp, end = 16.dp), - color = Color.White, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MovieTheme.typography.bodySmall, - ) - } - } -} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/MovieList.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/MovieList.kt index 264214b9..a5ab1375 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/MovieList.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/MovieList.kt @@ -18,26 +18,24 @@ package soup.movie.feature.home.impl.tab import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import soup.movie.core.designsystem.icon.MovieIcons import soup.movie.core.designsystem.theme.MovieTheme import soup.movie.core.imageloading.AsyncImage import soup.movie.domain.movie.getDDayLabel @@ -45,7 +43,7 @@ import soup.movie.domain.movie.isDDay import soup.movie.feature.home.impl.favorite.MovieAgeBadge import soup.movie.feature.home.impl.favorite.MovieDDayTag import soup.movie.model.MovieModel -import soup.movie.resources.R +import soup.movie.model.TheaterRatingsModel @Composable fun MovieList( @@ -87,6 +85,7 @@ private fun MovieItem( ) { Surface( modifier = modifier, + color = MaterialTheme.colorScheme.surfaceVariant, shape = MovieTheme.shapes.small, ) { Box { @@ -119,21 +118,42 @@ private fun MovieItem( } } +@PreviewLightDark @Composable -fun NoMovieItems( - modifier: Modifier = Modifier, +private fun MovieListPreview( + @PreviewParameter(MovieListPreviewParameterProvider::class) movies: List, ) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - MovieIcons.ViewModule, - contentDescription = null, - modifier = Modifier.size(72.dp), - ) - Text( - text = stringResource(R.string.no_movies_description), - ) + MovieTheme { + Surface { + MovieList( + movies = movies, + onItemClick = {}, + onLongItemClick = {}, + ) + } } } + +private class MovieListPreviewParameterProvider : PreviewParameterProvider> { + override val values: Sequence> = sequenceOf( + listOf(-1, 0, 11, 12, 14, 15, 18, 19, 20).mapIndexed { index, age -> + MovieModel( + id = index.toString(), + score = index, + title = "Movie Title", + posterUrl = "", + openDate = "2024.12.31", + isNow = false, + age = age, + nationFilter = null, + genres = null, + boxOffice = 0, + theater = TheaterRatingsModel( + cgv = null, + lotte = null, + megabox = null, + ), + ) + }, + ) +} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/NoMovieItems.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/NoMovieItems.kt new file mode 100644 index 00000000..7c392552 --- /dev/null +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/tab/NoMovieItems.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 SOUP + * + * 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 soup.movie.feature.home.impl.tab + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import soup.movie.core.designsystem.icon.MovieIcons +import soup.movie.core.designsystem.theme.MovieTheme +import soup.movie.resources.R + +@Composable +fun NoMovieItems( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + MovieIcons.ViewModule, + contentDescription = null, + modifier = Modifier.size(72.dp), + ) + Text( + text = stringResource(R.string.no_movies_description), + ) + } +} + +@PreviewLightDark +@Composable +private fun NoMovieItemsPreview() { + MovieTheme { + Surface { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + NoMovieItems( + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} diff --git a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchScreen.kt b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchScreen.kt index 3079234b..ed428cfa 100644 --- a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchScreen.kt +++ b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchScreen.kt @@ -16,6 +16,7 @@ package soup.movie.feature.search.impl import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -43,6 +44,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import soup.movie.core.designsystem.icon.MovieIcons import soup.movie.core.designsystem.showToast @@ -58,11 +60,51 @@ fun SearchScreen( onItemClick: (MovieModel) -> Unit, ) { val factory = rememberHomeComposableFactory() + val query by viewModel.query.collectAsState() + val uiModel by viewModel.uiModel.collectAsState() + SearchScaffold( + upPress = upPress, + query = query, + onQueryChanged = { viewModel.onQueryChanged(it) }, + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + if (uiModel.hasNoItem) { + factory.NoMovieItems(modifier = Modifier.align(Alignment.Center)) + } else { + val context = LocalContext.current + factory.MovieList( + movies = uiModel.movies, + onItemClick = { + onItemClick(it) + }, + onLongItemClick = { + context.showToast(it.title) + }, + modifier = Modifier, + ) + } + } + } +} + +@Composable +private fun SearchScaffold( + upPress: () -> Unit, + query: String, + onQueryChanged: (String) -> Unit, + content: @Composable (PaddingValues) -> Unit, +) { Scaffold( modifier = Modifier.systemBarsPadding(), topBar = { Surface( - modifier = Modifier.fillMaxWidth().height(56.dp), + modifier = Modifier + .fillMaxWidth() + .height(56.dp), color = MovieTheme.colors.primary, ) { val focusManager = LocalFocusManager.current @@ -71,11 +113,11 @@ fun SearchScreen( focusRequester.requestFocus() } - val query by viewModel.query.collectAsState() TextField( value = query, - onValueChange = { viewModel.onQueryChanged(it) }, - modifier = Modifier.fillMaxSize() + onValueChange = onQueryChanged, + modifier = Modifier + .fillMaxSize() .focusRequester(focusRequester), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, @@ -99,7 +141,7 @@ fun SearchScreen( } }, trailingIcon = { - IconButton(onClick = { viewModel.onQueryChanged("") }) { + IconButton(onClick = { onQueryChanged("") }) { Icon( MovieIcons.Close, contentDescription = null, @@ -111,33 +153,18 @@ fun SearchScreen( } }, ) { paddingValues -> - val uiModel by viewModel.uiModel.collectAsState() - when (uiModel) { - is SearchUiModel.None -> {} - is SearchUiModel.Success -> { - val model = uiModel as SearchUiModel.Success - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - ) { - if (model.hasNoItem) { - factory.NoMovieItems(modifier = Modifier.align(Alignment.Center)) - } else { - val context = LocalContext.current - factory.MovieList( - movies = model.movies, - onItemClick = { - onItemClick(it) - }, - onLongItemClick = { - context.showToast(it.title) - }, - modifier = Modifier, - ) - } - } - } - } + content(paddingValues) + } +} + +@PreviewLightDark +@Composable +private fun SearchScaffoldPreview() { + MovieTheme { + SearchScaffold( + upPress = {}, + query = "", + onQueryChanged = {}, + ) {} } } diff --git a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchUiModel.kt b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchUiModel.kt index 5474f674..414176ec 100644 --- a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchUiModel.kt +++ b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchUiModel.kt @@ -17,10 +17,14 @@ package soup.movie.feature.search.impl import soup.movie.model.MovieModel -sealed interface SearchUiModel { - data object None : SearchUiModel - data class Success( - val movies: List, - val hasNoItem: Boolean, - ) : SearchUiModel +data class SearchUiModel( + val movies: List, + val hasNoItem: Boolean, +) { + companion object { + val None = SearchUiModel( + movies = emptyList(), + hasNoItem = false, + ) + } } diff --git a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchViewModel.kt b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchViewModel.kt index 5e5e8bb1..71df5ed5 100644 --- a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchViewModel.kt +++ b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchViewModel.kt @@ -48,7 +48,7 @@ class SearchViewModel @Inject constructor( .flatMapLatest { query -> val movies = repository.searchMovie(query) flowOf( - SearchUiModel.Success( + SearchUiModel( movies = movies, hasNoItem = query.isNotEmpty() && movies.isEmpty(), ), diff --git a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/theme/ThemeOptionScreen.kt b/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/theme/ThemeOptionScreen.kt index ebc0ace4..6b98a5af 100644 --- a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/theme/ThemeOptionScreen.kt +++ b/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/theme/ThemeOptionScreen.kt @@ -30,10 +30,10 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import soup.movie.core.designsystem.theme.MovieTheme -import soup.movie.core.designsystem.tools.DevicePreviews import soup.movie.core.designsystem.util.debounce import soup.movie.feature.theme.ThemeOption import soup.movie.resources.R @@ -93,7 +93,7 @@ private fun ThemeOptionItem( } } -@DevicePreviews +@PreviewLightDark @Composable private fun ThemeOptionScreenPreview() { MovieTheme {