From d4c5a0eb11a1f80f076b2cc7ed31346518ae50f2 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:20:25 +0900 Subject: [PATCH 01/12] =?UTF-8?q?Dao=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/local/dao/FavoriteGameDao.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/data/local/dao/FavoriteGameDao.kt b/app/src/main/kotlin/com/lilin/gamelibrary/data/local/dao/FavoriteGameDao.kt index ac676ac..6905ade 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/data/local/dao/FavoriteGameDao.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/data/local/dao/FavoriteGameDao.kt @@ -17,6 +17,22 @@ interface FavoriteGameDao { @Query("SELECT * FROM favorite_games ORDER BY addedAt ASC") fun getFavoriteGamesOldestFirst(): Flow> + // タイトル(A-Z) + @Query("SELECT * FROM favorite_games ORDER BY name ASC") + fun getFavoriteGamesByTitleDesc(): Flow> + + // タイトル(Z-A) + @Query("SELECT * FROM favorite_games ORDER BY name DESC") + fun getFavoriteGamesByTitleAsc(): Flow> + + // 評価(高い順) + @Query("SELECT * FROM favorite_games ORDER BY rating DESC") + fun getFavoriteGamesByRatingDesc(): Flow> + + // 評価(低い順) + @Query("SELECT * FROM favorite_games ORDER BY rating ASC") + fun getFavoriteGamesByRatingAsc(): Flow> + // お気に入り判定用 @Query("SELECT EXISTS(SELECT 1 FROM favorite_games WHERE id = :gameId)") fun isFavorite(gameId: Int): Flow From 981dae0ff348584d7ea209dd1fb761738d38db8c Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:20:50 +0900 Subject: [PATCH 02/12] =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=A6=81=E7=B4=A0=E3=81=AE=E8=BF=BD=E5=8A=A0=E7=A7=BB=E8=A1=8C?= =?UTF-8?q?=E3=81=AE=E3=81=9F=E3=82=81=E3=81=AB=E5=88=A5=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/FavoriteStatistics.kt | 7 ++++++ .../gamelibrary/domain/model/SortOption.kt | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/domain/model/FavoriteStatistics.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/FavoriteStatistics.kt b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/FavoriteStatistics.kt new file mode 100644 index 0000000..c8b706d --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/FavoriteStatistics.kt @@ -0,0 +1,7 @@ +package com.lilin.gamelibrary.domain.model + +data class FavoriteStatistics( + val totalCount: Int, + val avgRating: Double, + val latestAdded: String, +) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt new file mode 100644 index 0000000..f85181e --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt @@ -0,0 +1,23 @@ +package com.lilin.gamelibrary.domain.model + +import com.lilin.gamelibrary.R + +enum class SortOption { + ADDED_DATE_DESC, + ADDED_DATE_ASC, + TITLE_ASC, + TITLE_DESC, + RATING_DESC, + RATING_ASC + ; + + val label: Int + get() = when (this) { + ADDED_DATE_DESC -> R.string.favorite_added_date_desc + ADDED_DATE_ASC -> R.string.favorite_added_date_asc + TITLE_ASC -> R.string.favorite_title_asc + TITLE_DESC -> R.string.favorite_title_desc + RATING_DESC -> R.string.favorite_rating_desc + RATING_ASC -> R.string.favorite_rating_asc + } +} From e873a73fbc73dbe891f6d438bc59a7023589adf8 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:21:38 +0900 Subject: [PATCH 03/12] =?UTF-8?q?Repository=E3=81=AE=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=A7=BB=E8=A1=8C=E3=81=AE=E3=81=9F=E3=82=81=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E9=81=A9=E5=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/FavoriteGameRepository.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/data/repository/FavoriteGameRepository.kt b/app/src/main/kotlin/com/lilin/gamelibrary/data/repository/FavoriteGameRepository.kt index dca9218..b301659 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/data/repository/FavoriteGameRepository.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/data/repository/FavoriteGameRepository.kt @@ -3,6 +3,7 @@ package com.lilin.gamelibrary.data.repository import com.lilin.gamelibrary.data.local.dao.FavoriteGameDao import com.lilin.gamelibrary.data.local.entity.FavoriteGameEntity import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.domain.model.SortOption import com.lilin.gamelibrary.domain.model.SortOrder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -20,6 +21,19 @@ class FavoriteGameRepository @Inject constructor( } } + fun getFavoriteGames2(order: SortOption): Flow> { + return when (order) { + SortOption.ADDED_DATE_DESC -> favoriteGameDao.getFavoriteGamesNewestFirst() + SortOption.ADDED_DATE_ASC -> favoriteGameDao.getFavoriteGamesOldestFirst() + SortOption.TITLE_DESC -> favoriteGameDao.getFavoriteGamesByTitleDesc() + SortOption.TITLE_ASC -> favoriteGameDao.getFavoriteGamesByTitleAsc() + SortOption.RATING_DESC -> favoriteGameDao.getFavoriteGamesByRatingDesc() + SortOption.RATING_ASC -> favoriteGameDao.getFavoriteGamesByRatingAsc() + }.map { entities -> + entities.map { it.toDomainModel() } + } + } + fun isFavorite(gameId: Int): Flow { return favoriteGameDao.isFavorite(gameId) } From 55cbc9283e29f8cd8400d9ba50e2fc3b16bc6329 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:22:37 +0900 Subject: [PATCH 04/12] =?UTF-8?q?Usecase=E4=BD=9C=E6=88=90=E7=A7=BB?= =?UTF-8?q?=E8=A1=8C=E3=81=AE=E3=81=9F=E3=82=81=E3=80=81invoke=E3=81=AE?= =?UTF-8?q?=E3=81=BE=E3=81=BE=E9=81=A9=E5=BD=93=E3=81=AB=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/usecase/favorite/GetFavoriteGamesUseCase.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/domain/usecase/favorite/GetFavoriteGamesUseCase.kt b/app/src/main/kotlin/com/lilin/gamelibrary/domain/usecase/favorite/GetFavoriteGamesUseCase.kt index c7913a5..51957a9 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/domain/usecase/favorite/GetFavoriteGamesUseCase.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/domain/usecase/favorite/GetFavoriteGamesUseCase.kt @@ -2,6 +2,7 @@ package com.lilin.gamelibrary.domain.usecase.favorite import com.lilin.gamelibrary.data.repository.FavoriteGameRepository import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.domain.model.SortOption import com.lilin.gamelibrary.domain.model.SortOrder import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -18,7 +19,12 @@ import javax.inject.Inject class GetFavoriteGamesUseCase @Inject constructor( private val repository: FavoriteGameRepository, ) { - operator fun invoke(sortOrder: SortOrder): Flow> { + @Deprecated("use invoke2") + fun invoke(sortOrder: SortOrder): Flow> { return repository.getFavoriteGames(sortOrder) } + + fun invoke2(sortOption: SortOption): Flow> { + return repository.getFavoriteGames2(sortOption) + } } From f2e41749074465d7639f0b43e5f0408cbadf2d84 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:23:24 +0900 Subject: [PATCH 05/12] =?UTF-8?q?component=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/favorite/DeleteConfirmDialog.kt | 86 +++++++ .../expanded/FavoriteGameExpandedCard.kt | 230 ++++++++++++++++++ .../favorite/expanded/FavoriteSidebar.kt | 79 ++++++ .../favorite/expanded/FavoriteTopAppBar.kt | 80 ++++++ .../favorite/expanded/SortSection.kt | 92 +++++++ .../favorite/expanded/StatisticsSection.kt | 110 +++++++++ app/src/main/res/values/strings.xml | 12 + 7 files changed, 689 insertions(+) create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteGameExpandedCard.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteSidebar.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteTopAppBar.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt new file mode 100644 index 0000000..ea05662 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt @@ -0,0 +1,86 @@ +package com.lilin.gamelibrary.ui.component.favorite + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.lilin.gamelibrary.R +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@Composable +fun DeleteConfirmDialog( + gameName: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + modifier: Modifier = Modifier, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(R.string.favorite_alert_dialog_title), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + text = { + Text( + text = stringResource(R.string.favorite_alert_dialog_message, gameName), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + confirmButton = { + TextButton( + onClick = { + onConfirm() + onDismiss() + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ) + ) { + Text(text = stringResource(R.string.favorite_alert_dialog_delete)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(R.string.favorite_alert_dialog_cancel)) + } + }, + modifier = modifier, + ) +} + +@Preview +@Composable +private fun DeleteConfirmDialogPreview() { + GameLibraryTheme { + DeleteConfirmDialog( + gameName = "Grand Theft Auto V", + onDismiss = {}, + onConfirm = {}, + ) + } +} + +@Preview +@Composable +private fun DeleteConfirmDialogLongNamePreview() { + GameLibraryTheme { + DeleteConfirmDialog( + gameName = "The Legend of Zelda: Breath of the Wild - Complete Edition", + onDismiss = {}, + onConfirm = {}, + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteGameExpandedCard.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteGameExpandedCard.kt new file mode 100644 index 0000000..49683aa --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteGameExpandedCard.kt @@ -0,0 +1,230 @@ +package com.lilin.gamelibrary.ui.component.favorite.expanded + +import androidx.compose.foundation.Image +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.aspectRatio +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.lilin.gamelibrary.R +import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme +import com.lilin.gamelibrary.ui.util.formatAvgRating + +@Composable +fun FavoriteGameExpandedCard( + game: FavoriteGame, + onClick: (Int) -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .clickable { onClick(game.id) }, + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f), + ) { + if (game.backgroundImage.isNullOrBlank()) { + Image( + painter = painterResource(R.drawable.ic_broken_image), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } else { + AsyncImage( + model = game.backgroundImage, + contentDescription = game.name, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + ) { + Text( + text = game.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = FavoriteGradientEnd, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = game.rating.formatAvgRating(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.CalendarToday, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = game.released ?: "未定", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + TextButton( + onClick = onDelete, + modifier = Modifier.align(Alignment.End), + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "削除", + style = MaterialTheme.typography.labelMedium, + ) + } + } + } + } +} + +@Preview +@Composable +private fun FavoriteGameExpandedCardPreview() { + GameLibraryTheme { + FavoriteGameExpandedCard( + game = FavoriteGame( + id = 1, + name = "Grand Theft Auto V", + backgroundImage = null, + rating = 4.5, + metacritic = 97, + released = "2013-09-17", + addedAt = System.currentTimeMillis(), + ), + onClick = {}, + onDelete = {}, + modifier = Modifier + .width(300.dp) + .padding(16.dp), + ) + } +} + +@Preview +@Composable +private fun FavoriteGameExpandedCardLongTitlePreview() { + GameLibraryTheme { + FavoriteGameExpandedCard( + game = FavoriteGame( + id = 2, + name = "The Legend of Zelda: Breath of the Wild - Complete Edition with All DLCs", + backgroundImage = null, + rating = 4.8, + metacritic = 97, + released = "2017-03-03", + addedAt = System.currentTimeMillis(), + ), + onClick = {}, + onDelete = {}, + modifier = Modifier + .width(300.dp) + .padding(16.dp), + ) + } +} + +@Preview +@Composable +private fun FavoriteGameExpandedCardNoDataPreview() { + GameLibraryTheme { + FavoriteGameExpandedCard( + game = FavoriteGame( + id = 3, + name = "Cyberpunk 2077", + backgroundImage = null, + rating = 0.0, + metacritic = null, + released = null, + addedAt = System.currentTimeMillis(), + ), + onClick = {}, + onDelete = {}, + modifier = Modifier + .width(300.dp) + .padding(16.dp), + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteSidebar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteSidebar.kt new file mode 100644 index 0000000..fed061d --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteSidebar.kt @@ -0,0 +1,79 @@ +package com.lilin.gamelibrary.ui.component.favorite.expanded + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.domain.model.FavoriteStatistics +import com.lilin.gamelibrary.domain.model.SortOption +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@Composable +fun FavoriteSidebar( + currentSort: SortOption, + statistics: FavoriteStatistics, + onSortChange: (SortOption) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxHeight() + .background(MaterialTheme.colorScheme.surfaceVariant) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + ) { + SortSection( + currentSort = currentSort, + onSortChange = onSortChange, + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + + StatisticsSection( + statistics = statistics, + ) + } +} + +@Preview(showBackground = true, widthDp = 280, heightDp = 800) +@Composable +private fun FavoriteSidebarPreview() { + GameLibraryTheme { + FavoriteSidebar( + currentSort = SortOption.ADDED_DATE_DESC, + statistics = FavoriteStatistics( + totalCount = 125, + avgRating = 4.6, + latestAdded = "2日前", + ), + onSortChange = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 280, heightDp = 800) +@Composable +private fun FavoriteSidebarEmptyPreview() { + GameLibraryTheme { + FavoriteSidebar( + currentSort = SortOption.RATING_DESC, + statistics = FavoriteStatistics( + totalCount = 0, + avgRating = 0.0, + latestAdded = "-", + ), + onSortChange = {}, + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteTopAppBar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteTopAppBar.kt new file mode 100644 index 0000000..099103d --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/FavoriteTopAppBar.kt @@ -0,0 +1,80 @@ +package com.lilin.gamelibrary.ui.component.favorite.expanded + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FavoriteTopAppBar( + title: String, + itemCount: Int, + modifier: Modifier = Modifier, +) { + TopAppBar( + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = "${itemCount}件", + style = MaterialTheme.typography.bodyMedium, + color = FavoriteGradientEnd, + ) + } + }, + modifier = modifier, + ) +} + +@Preview +@Composable +private fun FavoriteTopAppBarPreview() { + GameLibraryTheme { + FavoriteTopAppBar( + title = "お気に入り", + itemCount = 125, + ) + } +} + +@Preview +@Composable +private fun FavoriteTopAppBarEmptyPreview() { + GameLibraryTheme { + FavoriteTopAppBar( + title = "お気に入り", + itemCount = 0, + ) + } +} + +@Preview +@Composable +private fun FavoriteTopAppBarLargeCountPreview() { + GameLibraryTheme { + FavoriteTopAppBar( + title = "お気に入り", + itemCount = 9999, + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt new file mode 100644 index 0000000..896e7d1 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt @@ -0,0 +1,92 @@ +package com.lilin.gamelibrary.ui.component.favorite.expanded + +import androidx.compose.foundation.clickable +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.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.R +import com.lilin.gamelibrary.domain.model.SortOption +import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@Composable +fun SortSection( + currentSort: SortOption, + onSortChange: (SortOption) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + Text( + text = stringResource(R.string.favorite_sort), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + Spacer(modifier = Modifier.padding(vertical = 4.dp)) + SortOption.entries.forEach { option -> + SortOptionItem( + option = option, + isSelected = option == currentSort, + onClick = { onSortChange(option) }, + ) + } + } +} + +@Composable +private fun SortOptionItem( + option: SortOption, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 4.dp), + ) { + RadioButton( + selected = isSelected, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = FavoriteGradientEnd, + ), + ) + + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = option.label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SortSectionPreview() { + GameLibraryTheme { + SortSection( + currentSort = SortOption.ADDED_DATE_DESC, + onSortChange = {}, + modifier = Modifier.padding(16.dp), + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt new file mode 100644 index 0000000..38b7541 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt @@ -0,0 +1,110 @@ +package com.lilin.gamelibrary.ui.component.favorite.expanded + +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.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.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.domain.model.FavoriteStatistics +import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme +import com.lilin.gamelibrary.ui.util.formatAvgRatingWithStar + +@Composable +fun StatisticsSection( + statistics: FavoriteStatistics, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + Text( + text = "統計", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + Spacer(modifier = Modifier.height(12.dp)) + StatisticItem( + label = "合計", + value = statistics.totalCount.toString(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + StatisticItem( + label = "平均評価", + value = statistics.avgRating.formatAvgRatingWithStar(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + StatisticItem( + label = "最新追加", + value = statistics.latestAdded, + ) + } +} + +@Composable +private fun StatisticItem( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = FavoriteGradientEnd, + ) + } + +} + +@Preview(showBackground = true) +@Composable +private fun StatisticsSectionPreview() { + GameLibraryTheme { + StatisticsSection( + statistics = FavoriteStatistics( + totalCount = 125, + avgRating = 4.6, + latestAdded = "2日前", + ), + modifier = Modifier.padding(16.dp), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun StatisticsSectionEmptyPreview() { + GameLibraryTheme { + StatisticsSection( + statistics = FavoriteStatistics( + totalCount = 0, + avgRating = 0.0, + latestAdded = "-", + ), + modifier = Modifier.padding(16.dp), + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01d3437..eb6642c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,4 +72,16 @@ プラットフォーム情報 検索結果: %s - %d 件 更新 + + 追加日(新しい順) + 追加日(古い順) + タイトル(A-Z) + タイトル(Z-A) + 評価(高い順) + 評価(低い順) + 並び替え + 削除の確認 + 「%s」をお気に入りから削除しますか? + キャンセル + 削除 From 08a8a5615bd49edd254aaf0f6875fb8fa9ab6496 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:23:33 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...teGameCard.kt => FavoriteGameCompactCard.kt} | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) rename app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/{FavoriteGameCard.kt => FavoriteGameCompactCard.kt} (95%) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCard.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt similarity index 95% rename from app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCard.kt rename to app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt index ffce5c3..78eaab1 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCard.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt @@ -61,7 +61,7 @@ private const val METACRITIC_SILVER_THRESHOLD = 70 private const val METACRITIC_BRONZE_THRESHOLD = 50 @Composable -fun FavoriteGameCard( +fun FavoriteGameCompactCard( game: FavoriteGame, onClick: (Int) -> Unit, onClickDelete: (Int) -> Unit, @@ -199,7 +199,11 @@ fun FavoriteGameCard( verticalAlignment = Alignment.CenterVertically, ) { LabeledIcon( - content = String.format(Locale.current.platformLocale, "%.1f", game.rating), + content = String.format( + Locale.current.platformLocale, + "%.1f", + game.rating, + ), imageVector = Icons.Rounded.Star, contentDescription = "Rating", ) @@ -209,6 +213,7 @@ fun FavoriteGameCard( Box( modifier = Modifier + .align(Alignment.CenterVertically) .size(48.dp) .clip(RoundedCornerShape(12.dp)) .background( @@ -239,7 +244,7 @@ fun FavoriteGameCard( @Preview(showBackground = true) @Composable -private fun FavoriteGameCardPreview() { +private fun FavoriteGameCompactCardPreview() { val game = FavoriteGame( id = 1, name = "The Legend of Zelda: Breath of the Wild", @@ -249,7 +254,7 @@ private fun FavoriteGameCardPreview() { released = "2017-03-03", addedAt = System.currentTimeMillis(), ) - FavoriteGameCard( + FavoriteGameCompactCard( game = game, onClick = {}, onClickDelete = {}, @@ -258,7 +263,7 @@ private fun FavoriteGameCardPreview() { @Preview(showBackground = true, name = "Low Score") @Composable -private fun FavoriteGameCardLowScorePreview() { +private fun FavoriteGameCompactCardLowScorePreview() { val game = FavoriteGame( id = 2, name = "Low Score Game with Very Long Title That Should Be Truncated", @@ -268,7 +273,7 @@ private fun FavoriteGameCardLowScorePreview() { released = "2023-12-01", addedAt = System.currentTimeMillis(), ) - FavoriteGameCard( + FavoriteGameCompactCard( game = game, onClick = {}, onClickDelete = {}, From 43b2f0524309ff24e805c09c85ed8781ec806ba2 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:23:57 +0900 Subject: [PATCH 07/12] =?UTF-8?q?formatter=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lilin/gamelibrary/ui/util/Formatter.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt new file mode 100644 index 0000000..c5726e3 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt @@ -0,0 +1,19 @@ +package com.lilin.gamelibrary.ui.util + +fun Double.formatAvgRating(): String { + return if (this > 0) { + "%.1f".format(this) + } else { + "-" + } +} + +fun Double.formatAvgRatingWithStar(): String { + return if (this > 0) { + "★ ${"%.1f".format(this)}" + } else { + "-" + } +} + + From 6b3e1629a8becc2d8cbeace46512f518fedf3e9f Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:24:21 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=E5=A4=A7=E3=81=8D=E3=81=84=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E7=94=A8=E3=81=AE=E3=83=AC=E3=82=A4=E3=82=A2=E3=82=A6?= =?UTF-8?q?=E3=83=88=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expanded/FavoriteExpandedLayout.kt | 217 ++++++++++++++++++ .../feature/favorite/expanded/FavoriteGrid.kt | 209 +++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteExpandedLayout.kt create mode 100644 app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteGrid.kt diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteExpandedLayout.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteExpandedLayout.kt new file mode 100644 index 0000000..75b59c4 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteExpandedLayout.kt @@ -0,0 +1,217 @@ +package com.lilin.gamelibrary.feature.favorite.expanded + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.R +import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.domain.model.FavoriteStatistics +import com.lilin.gamelibrary.domain.model.SortOption +import com.lilin.gamelibrary.feature.favorite.FavoriteExpandedUiState +import com.lilin.gamelibrary.ui.component.favorite.DeleteConfirmDialog +import com.lilin.gamelibrary.ui.component.favorite.expanded.FavoriteSidebar +import com.lilin.gamelibrary.ui.component.favorite.expanded.FavoriteTopAppBar +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@Composable +fun FavoriteExpandedLayout( + uiState: FavoriteExpandedUiState, + bottomBarPadding: Dp, + onSortChange: (SortOption) -> Unit, + onGameClick: (Int) -> Unit, + onDeleteClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxSize(), + ) { + FavoriteSidebar( + currentSort = when (uiState) { + is FavoriteExpandedUiState.Success -> uiState.sortOption + else -> SortOption.ADDED_DATE_DESC + }, + statistics = when (uiState) { + is FavoriteExpandedUiState.Success -> uiState.statistics + else -> FavoriteStatistics( + totalCount = 0, + avgRating = 0.0, + latestAdded = "-", + ) + }, + onSortChange = onSortChange, + modifier = Modifier.weight(0.3f), + ) + + VerticalDivider( + modifier = Modifier.fillMaxHeight(), + thickness = 1.dp, + ) + + Column( + modifier = Modifier.weight(0.7f), + ) { + FavoriteTopAppBar( + title = stringResource(R.string.favorite_screen_title), + itemCount = when (uiState) { + is FavoriteExpandedUiState.Success -> uiState.games.size + else -> 0 + }, + ) + + FavoriteGrid( + uiState = uiState, + bottomBarPadding = bottomBarPadding, + onGameClick = onGameClick, + onDeleteClick = onDeleteClick, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 1200, heightDp = 800) +@Composable +private fun FavoriteExpandedLayoutLoadingPreview() { + GameLibraryTheme { + FavoriteExpandedLayout( + uiState = FavoriteExpandedUiState.Loading, + bottomBarPadding = 0.dp, + onSortChange = {}, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 1200, heightDp = 800) +@Composable +private fun FavoriteExpandedLayoutEmptyPreview() { + GameLibraryTheme { + FavoriteExpandedLayout( + uiState = FavoriteExpandedUiState.Empty, + bottomBarPadding = 0.dp, + onSortChange = {}, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 1200, heightDp = 800) +@Composable +private fun FavoriteExpandedLayoutSuccessPreview() { + val sampleGames = listOf( + FavoriteGame( + id = 1, + name = "Grand Theft Auto V", + backgroundImage = null, + rating = 4.5, + metacritic = 97, + released = "2013-09-17", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 2, + name = "The Witcher 3", + backgroundImage = null, + rating = 4.8, + metacritic = 93, + released = "2015-05-19", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 3, + name = "Red Dead Redemption 2", + backgroundImage = null, + rating = 4.6, + metacritic = 97, + released = "2018-10-26", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 4, + name = "Portal 2", + backgroundImage = null, + rating = 4.7, + metacritic = 95, + released = "2011-04-19", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 5, + name = "Half-Life 2", + backgroundImage = null, + rating = 4.6, + metacritic = 96, + released = "2004-11-16", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 6, + name = "Minecraft", + backgroundImage = null, + rating = 4.4, + metacritic = 93, + released = "2011-11-18", + addedAt = System.currentTimeMillis(), + ), + ) + + GameLibraryTheme { + var showDialog by remember { mutableStateOf(false) } + var selectedGame by remember { mutableStateOf(null) } + + FavoriteExpandedLayout( + uiState = FavoriteExpandedUiState.Success( + games = sampleGames, + statistics = FavoriteStatistics( + totalCount = sampleGames.size, + avgRating = 4.6, + latestAdded = "2日前", + ), + sortOption = SortOption.ADDED_DATE_DESC, + ), + bottomBarPadding = 0.dp, + onSortChange = {}, + onGameClick = {}, + onDeleteClick = {}, + ) + + if (showDialog && selectedGame != null) { + DeleteConfirmDialog( + gameName = selectedGame!!.name, + onDismiss = { showDialog = false }, + onConfirm = { + showDialog = false + selectedGame = null + }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 1200, heightDp = 800) +@Composable +private fun FavoriteExpandedLayoutErrorPreview() { + GameLibraryTheme { + FavoriteExpandedLayout( + uiState = FavoriteExpandedUiState.Error("データの読み込みに失敗しました"), + bottomBarPadding = 0.dp, + onSortChange = {}, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteGrid.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteGrid.kt new file mode 100644 index 0000000..7547db0 --- /dev/null +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/expanded/FavoriteGrid.kt @@ -0,0 +1,209 @@ +package com.lilin.gamelibrary.feature.favorite.expanded + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.feature.favorite.FavoriteEmptyContent +import com.lilin.gamelibrary.feature.favorite.FavoriteExpandedUiState +import com.lilin.gamelibrary.feature.favorite.FavoriteGridError +import com.lilin.gamelibrary.feature.favorite.FavoriteGridLoading +import com.lilin.gamelibrary.ui.component.favorite.DeleteConfirmDialog +import com.lilin.gamelibrary.ui.component.favorite.expanded.FavoriteGameExpandedCard +import com.lilin.gamelibrary.ui.theme.GameLibraryTheme + +@Composable +fun FavoriteGrid( + uiState: FavoriteExpandedUiState, + bottomBarPadding: Dp, + onGameClick: (Int) -> Unit, + onDeleteClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + when (uiState) { + is FavoriteExpandedUiState.Empty -> { + FavoriteEmptyContent() + } + + is FavoriteExpandedUiState.Loading -> { + FavoriteGridLoading() + } + + is FavoriteExpandedUiState.Success -> { + FavoriteGridSuccess( + games = uiState.games, + bottomBarPadding = bottomBarPadding, + onGameClick = onGameClick, + onDeleteClick = onDeleteClick, + modifier = modifier, + ) + } + + is FavoriteExpandedUiState.Error -> { + FavoriteGridError( + message = uiState.message, + ) + } + } +} + +@Composable +private fun FavoriteGridSuccess( + games: List, + bottomBarPadding: Dp, + onGameClick: (Int) -> Unit, + onDeleteClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + val gridScrollState = rememberLazyGridState() + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier + .fillMaxSize(), + state = gridScrollState, + contentPadding = PaddingValues( + start = 10.dp, + end = 10.dp, + bottom = bottomBarPadding, + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items( + items = games, + key = { it.id }, + ) { + FavoriteGameExpandedCard( + game = it, + onClick = onGameClick, + onDelete = { + onDeleteClick(it.id) + }, + modifier = Modifier.animateItem(), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 800, heightDp = 600) +@Composable +private fun FavoriteGridLoadingPreview() { + GameLibraryTheme { + FavoriteGrid( + uiState = FavoriteExpandedUiState.Loading, + bottomBarPadding = 16.dp, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 800, heightDp = 600) +@Composable +private fun FavoriteGridEmptyPreview() { + GameLibraryTheme { + FavoriteGrid( + uiState = FavoriteExpandedUiState.Empty, + bottomBarPadding = 0.dp, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 800, heightDp = 600) +@Composable +private fun FavoriteGridSuccessPreview() { + val sampleGames = listOf( + FavoriteGame( + id = 1, + name = "Grand Theft Auto V", + backgroundImage = null, + rating = 4.5, + metacritic = 97, + released = "2013-09-17", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 2, + name = "The Witcher 3", + backgroundImage = null, + rating = 4.8, + metacritic = 93, + released = "2015-05-19", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 3, + name = "Red Dead Redemption 2", + backgroundImage = null, + rating = 4.6, + metacritic = 97, + released = "2018-10-26", + addedAt = System.currentTimeMillis(), + ), + FavoriteGame( + id = 4, + name = "Portal 2", + backgroundImage = null, + rating = 4.7, + metacritic = 95, + released = "2011-04-19", + addedAt = System.currentTimeMillis(), + ), + ) + + GameLibraryTheme { + var showDialog by remember { mutableStateOf(false) } + + FavoriteGrid( + uiState = FavoriteExpandedUiState.Success( + games = sampleGames, + statistics = com.lilin.gamelibrary.domain.model.FavoriteStatistics( + totalCount = sampleGames.size, + avgRating = 4.6, + latestAdded = "2日前", + ), + sortOption = com.lilin.gamelibrary.domain.model.SortOption.ADDED_DATE_DESC, + ), + bottomBarPadding = 0.dp, + onGameClick = {}, + onDeleteClick = { showDialog = true }, + ) + + if (showDialog) { + DeleteConfirmDialog( + gameName = "Grand Theft Auto V", + onDismiss = { showDialog = false }, + onConfirm = { showDialog = false }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 800, heightDp = 600) +@Composable +private fun FavoriteGridErrorPreview() { + GameLibraryTheme { + FavoriteGrid( + uiState = FavoriteExpandedUiState.Error("データの読み込みに失敗しました"), + bottomBarPadding = 0.dp, + onGameClick = {}, + onDeleteClick = {}, + ) + } +} From c672059141a225381c48a7c6c2ed44224813940d Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:24:40 +0900 Subject: [PATCH 09/12] =?UTF-8?q?=E5=A4=A7=E3=81=8D=E3=81=84=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/favorite/FavoriteScreen.kt | 156 ++++++++++++------ .../feature/favorite/FavoriteUiState.kt | 16 ++ .../feature/favorite/FavoriteViewModel.kt | 136 ++++++++++++++- .../lilin/gamelibrary/ui/GameLibraryApp.kt | 1 + 4 files changed, 255 insertions(+), 54 deletions(-) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt index b1a1a9f..1a6ebfa 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt @@ -45,9 +45,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.lilin.gamelibrary.R -import com.lilin.gamelibrary.ui.component.favorite.FavoriteGameCard +import com.lilin.gamelibrary.feature.favorite.expanded.FavoriteExpandedLayout +import com.lilin.gamelibrary.ui.component.favorite.FavoriteGameCompactCard import com.lilin.gamelibrary.ui.component.favorite.FavoriteScreenHeader import com.lilin.gamelibrary.ui.component.favorite.FavoriteSortFabMenu +import com.lilin.gamelibrary.ui.component.favorite.DeleteConfirmDialog import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd import com.lilin.gamelibrary.ui.theme.FavoriteGradientStart import kotlinx.serialization.Serializable @@ -56,10 +58,12 @@ import kotlinx.serialization.Serializable object FavoriteScreen fun NavGraphBuilder.navigateFavoriteScreen( + isAtLeastMedium: Boolean, navigateToDetail: (Int) -> Unit, ) { composable { FavoriteScreen( + isAtLeastMedium = isAtLeastMedium, navigateToDetail = { gameId -> navigateToDetail(gameId) }, @@ -70,6 +74,7 @@ fun NavGraphBuilder.navigateFavoriteScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun FavoriteScreen( + isAtLeastMedium: Boolean, navigateToDetail: (Int) -> Unit, modifier: Modifier = Modifier, viewModel: FavoriteViewModel = hiltViewModel(), @@ -77,35 +82,63 @@ fun FavoriteScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val sortOrder by viewModel.sortOrder.collectAsStateWithLifecycle() + val expandedUiState by viewModel.expandedUiState.collectAsStateWithLifecycle() + val showDeleteDialog by viewModel.showDeleteDialog.collectAsStateWithLifecycle() + val selectedGameName by viewModel.selectedGameName.collectAsStateWithLifecycle() + Scaffold( topBar = { - CenterAlignedTopAppBar( - title = { - Text( - text = stringResource(R.string.favorite_screen_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - ) - }, - ) + if (!isAtLeastMedium) { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(R.string.favorite_screen_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + }, + ) + } }, floatingActionButton = { - FavoriteSortFabMenu( - sortOrder = sortOrder, - onSortClick = viewModel::toggleSortOrder, - ) + if (!isAtLeastMedium) { + FavoriteSortFabMenu( + sortOrder = sortOrder, + onSortClick = viewModel::toggleSortOrder, + ) + } }, contentWindowInsets = WindowInsets.navigationBars, modifier = modifier, ) { paddingValues -> - FavoriteScreen( - uiState = uiState, - navigateToDetail = { gameId -> - navigateToDetail(gameId) - }, - onClickDelete = viewModel::removeFavoriteGame, - modifier = Modifier.padding(paddingValues), - ) + if (isAtLeastMedium) { + FavoriteExpandedLayout( + uiState = expandedUiState, + bottomBarPadding = paddingValues.calculateBottomPadding(), + onSortChange = viewModel::onSortChange, + onGameClick = { gameId -> + navigateToDetail(gameId) + }, + onDeleteClick = viewModel::showDeleteConfirmDialog, + ) + } else { + FavoriteScreen( + uiState = uiState, + navigateToDetail = { gameId -> + navigateToDetail(gameId) + }, + onClickDelete = viewModel::showDeleteConfirmDialog, + modifier = Modifier.padding(paddingValues), + ) + } + + if (showDeleteDialog) { + DeleteConfirmDialog( + gameName = selectedGameName, + onDismiss = viewModel::dismissDeleteDialog, + onConfirm = viewModel::confirmDelete, + ) + } } } @@ -124,16 +157,7 @@ private fun FavoriteScreen( } is FavoriteUiState.Loading -> { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - modifier = Modifier.size(64.dp), - color = FavoriteGradientEnd, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } + FavoriteGridLoading() } is FavoriteUiState.Success -> { @@ -163,7 +187,7 @@ private fun FavoriteScreen( items = uiState.games, key = { it.id }, ) { - FavoriteGameCard( + FavoriteGameCompactCard( game = it, onClick = { gameId -> navigateToDetail(gameId) @@ -178,31 +202,31 @@ private fun FavoriteScreen( } is FavoriteUiState.Error -> { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Icon( - imageVector = Icons.Outlined.ErrorOutline, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.error, - ) - - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = uiState.message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - ) - } + FavoriteGridError( + message = uiState.message, + ) } } } @Composable -private fun FavoriteEmptyContent( +fun FavoriteGridLoading( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp), + color = FavoriteGradientEnd, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } +} + +@Composable +fun FavoriteEmptyContent( modifier: Modifier = Modifier, ) { Column( @@ -252,6 +276,32 @@ private fun FavoriteEmptyContent( } } +@Composable +fun FavoriteGridError( + message: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.ErrorOutline, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.error, + ) + + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } +} + @Preview @Composable private fun FavoriteEmptyContentPreview() { diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteUiState.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteUiState.kt index 71d69e6..916a778 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteUiState.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteUiState.kt @@ -1,6 +1,8 @@ package com.lilin.gamelibrary.feature.favorite import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.domain.model.FavoriteStatistics +import com.lilin.gamelibrary.domain.model.SortOption import com.lilin.gamelibrary.domain.model.SortOrder sealed interface FavoriteUiState { @@ -15,3 +17,17 @@ sealed interface FavoriteUiState { data class Error(val message: String) : FavoriteUiState } + +sealed interface FavoriteExpandedUiState { + data object Empty : FavoriteExpandedUiState + + data object Loading : FavoriteExpandedUiState + + data class Success( + val games: List, + val statistics: FavoriteStatistics, + val sortOption: SortOption, + ) : FavoriteExpandedUiState + + data class Error(val message: String) : FavoriteExpandedUiState +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt index 203247a..563c06c 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt @@ -2,6 +2,9 @@ package com.lilin.gamelibrary.feature.favorite import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lilin.gamelibrary.domain.model.FavoriteGame +import com.lilin.gamelibrary.domain.model.FavoriteStatistics +import com.lilin.gamelibrary.domain.model.SortOption import com.lilin.gamelibrary.domain.model.SortOrder import com.lilin.gamelibrary.domain.usecase.favorite.GetFavoriteGamesUseCase import com.lilin.gamelibrary.domain.usecase.favorite.RemoveFavoriteGameUseCase @@ -10,6 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel @@ -20,11 +24,26 @@ class FavoriteViewModel @Inject constructor( private val _uiState = MutableStateFlow(FavoriteUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() + private val _expandedUiState = + MutableStateFlow(FavoriteExpandedUiState.Empty) + val expandedUiState: StateFlow = _expandedUiState.asStateFlow() + + private val _sortOrder = MutableStateFlow(SortOrder.NEWEST_FIRST) val sortOrder: StateFlow = _sortOrder.asStateFlow() + private val _showDeleteDialog = MutableStateFlow(false) + val showDeleteDialog: StateFlow = _showDeleteDialog.asStateFlow() + + private val _selectedGameId = MutableStateFlow(0) + val selectedGameId: StateFlow = _selectedGameId.asStateFlow() + + private val _selectedGameName = MutableStateFlow("") + val selectedGameName: StateFlow = _selectedGameName.asStateFlow() + init { getFavoriteGames(SortOrder.NEWEST_FIRST) + loadFavoriteGamesExpanded(SortOption.ADDED_DATE_DESC) } private fun getFavoriteGames(sortOrder: SortOrder) { @@ -32,7 +51,7 @@ class FavoriteViewModel @Inject constructor( viewModelScope.launch { runCatching { - getFavoriteGamesUseCase(sortOrder).collect { games -> + getFavoriteGamesUseCase.invoke(sortOrder).collect { games -> _uiState.value = if (games.isEmpty()) { FavoriteUiState.Empty } else { @@ -66,4 +85,119 @@ class FavoriteViewModel @Inject constructor( } } } + + // Expanded + private fun loadFavoriteGamesExpanded(sortOption: SortOption) { + _expandedUiState.value = FavoriteExpandedUiState.Loading + + viewModelScope.launch { + runCatching { + getFavoriteGamesUseCase.invoke2(sortOption).collect { games -> + if (games.isEmpty()) { + _expandedUiState.value = FavoriteExpandedUiState.Empty + } else { + // 統計計算 + val statistics = calculateStatistics(games) + + _expandedUiState.value = FavoriteExpandedUiState.Success( + games = games, + statistics = statistics, + sortOption = sortOption, + ) + } + } + }.onFailure { + _expandedUiState.value = + FavoriteExpandedUiState.Error(it.message ?: "Unknown error") + } + } + } + + fun showDeleteConfirmDialog(gameId: Int) { + val gameName = when (val state = _expandedUiState.value) { + is FavoriteExpandedUiState.Success -> { + state.games.find { it.id == gameId }?.name ?: "" + } + else -> { + when (val compactState = _uiState.value) { + is FavoriteUiState.Success -> { + compactState.games.find { it.id == gameId }?.name ?: "" + } + else -> "" + } + } + } + + _selectedGameId.value = gameId + _selectedGameName.value = gameName + _showDeleteDialog.value = true + } + + fun dismissDeleteDialog() { + _showDeleteDialog.value = false + _selectedGameId.value = 0 + _selectedGameName.value = "" + } + + fun confirmDelete() { + val gameId = _selectedGameId.value + if (gameId > 0) { + viewModelScope.launch { + removeFavoriteGameUseCase(gameId) + } + } + dismissDeleteDialog() + } + + fun onSortChange(sortOption: SortOption) { + loadFavoriteGamesExpanded(sortOption) + } + + fun calculateStatistics(games: List): FavoriteStatistics { + if (games.isEmpty()) { + return FavoriteStatistics( + totalCount = 0, + avgRating = 0.0, + latestAdded = "-", + ) + } + val totalCount = games.size + val avgRating = games.map { it.rating }.average() + val latestGame = games.maxByOrNull { it.addedAt } + val latestAdded = latestGame?.let { + formatRelativeTime(it.addedAt) + } ?: "-" + + return FavoriteStatistics( + totalCount = totalCount, + avgRating = avgRating, + latestAdded = latestAdded, + ) + } + + /** + * タイムスタンプを相対時間に変換 + * + * @param timestamp Unix timestamp (milliseconds) + * @return 相対時間文字列(例: "2日前", "1週間前") + */ + private fun formatRelativeTime(timestamp: Long): String { + val now = System.currentTimeMillis() + val diff = now - timestamp + + val seconds = TimeUnit.MILLISECONDS.toSeconds(diff) + val minutes = TimeUnit.MILLISECONDS.toMinutes(diff) + val hours = TimeUnit.MILLISECONDS.toHours(diff) + val days = TimeUnit.MILLISECONDS.toDays(diff) + + return when { + seconds < 60 -> "たった今" + minutes < 60 -> "${minutes}分前" + hours < 24 -> "${hours}時間前" + days < 7 -> "${days}日前" + days < 30 -> "${days / 7}週間前" + days < 365 -> "${days / 30}ヶ月前" + else -> "${days / 365}年前" + } + } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/GameLibraryApp.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/GameLibraryApp.kt index a0dc96f..d23d419 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/GameLibraryApp.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/GameLibraryApp.kt @@ -324,6 +324,7 @@ private fun NavGraphBuilder.bottomNavGraph( ) navigateFavoriteScreen( + isAtLeastMedium = isAtLeastMedium, navigateToDetail = { gameId -> navController.navigate(GameDetailScreen(gameId)) }, From 7e14ee7ec892bcf72916c835364b095d11fdf84e Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:24:58 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AB=E3=82=88=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/favorite/FavoriteGameCardScreenShotTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCardScreenShotTest.kt b/app/src/test/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCardScreenShotTest.kt index a3d61e9..24f6f7a 100644 --- a/app/src/test/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCardScreenShotTest.kt +++ b/app/src/test/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCardScreenShotTest.kt @@ -53,7 +53,7 @@ class FavoriteGameCardScreenShotTest { @Test fun favoriteGameCard_withImage_screenShot() { composeTestRule.captureMultiDevice("FavoriteGameCard_WithImage") { - FavoriteGameCard( + FavoriteGameCompactCard( game = createMockFavoriteGame( name = "The Legend of Zelda: Breath of the Wild", backgroundImage = "https://example.com/image.jpg", @@ -70,7 +70,7 @@ class FavoriteGameCardScreenShotTest { @Test fun favoriteGameCard_withoutImage_screenShot() { composeTestRule.captureMultiDevice("FavoriteGameCard_WithoutImage") { - FavoriteGameCard( + FavoriteGameCompactCard( game = createMockFavoriteGame( name = "Stardew Valley", backgroundImage = null, @@ -87,7 +87,7 @@ class FavoriteGameCardScreenShotTest { @Test fun favoriteGameCard_longGameName_screenShot() { composeTestRule.captureMultiDevice("FavoriteGameCard_LongGameName") { - FavoriteGameCard( + FavoriteGameCompactCard( game = createMockFavoriteGame( name = "The Elder Scrolls V: Skyrim Special Edition Anniversary Edition", backgroundImage = "https://example.com/image.jpg", From 510126ee1012331fd78180d999e7647cb7feb558 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 22:46:17 +0900 Subject: [PATCH 11/12] =?UTF-8?q?detekt=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt | 2 +- .../com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt | 2 +- .../com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt | 2 +- .../gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt | 2 +- .../ui/component/favorite/expanded/StatisticsSection.kt | 1 - app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt | 2 -- 6 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt index f85181e..14f0c83 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/domain/model/SortOption.kt @@ -8,7 +8,7 @@ enum class SortOption { TITLE_ASC, TITLE_DESC, RATING_DESC, - RATING_ASC + RATING_ASC, ; val label: Int diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt index 1a6ebfa..828faa3 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt @@ -46,10 +46,10 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.lilin.gamelibrary.R import com.lilin.gamelibrary.feature.favorite.expanded.FavoriteExpandedLayout +import com.lilin.gamelibrary.ui.component.favorite.DeleteConfirmDialog import com.lilin.gamelibrary.ui.component.favorite.FavoriteGameCompactCard import com.lilin.gamelibrary.ui.component.favorite.FavoriteScreenHeader import com.lilin.gamelibrary.ui.component.favorite.FavoriteSortFabMenu -import com.lilin.gamelibrary.ui.component.favorite.DeleteConfirmDialog import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd import com.lilin.gamelibrary.ui.theme.FavoriteGradientStart import kotlinx.serialization.Serializable diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt index 563c06c..d7df2c1 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModel.kt @@ -28,7 +28,6 @@ class FavoriteViewModel @Inject constructor( MutableStateFlow(FavoriteExpandedUiState.Empty) val expandedUiState: StateFlow = _expandedUiState.asStateFlow() - private val _sortOrder = MutableStateFlow(SortOrder.NEWEST_FIRST) val sortOrder: StateFlow = _sortOrder.asStateFlow() @@ -181,6 +180,7 @@ class FavoriteViewModel @Inject constructor( * @param timestamp Unix timestamp (milliseconds) * @return 相対時間文字列(例: "2日前", "1週間前") */ + @Suppress("MagicNumber") private fun formatRelativeTime(timestamp: Long): String { val now = System.currentTimeMillis() val diff = now - timestamp diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt index ea05662..ca44dc7 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt @@ -47,7 +47,7 @@ fun DeleteConfirmDialog( }, colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.error, - ) + ), ) { Text(text = stringResource(R.string.favorite_alert_dialog_delete)) } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt index 38b7541..fb4bcc6 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt @@ -76,7 +76,6 @@ private fun StatisticItem( color = FavoriteGradientEnd, ) } - } @Preview(showBackground = true) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt index c5726e3..1efacbd 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/util/Formatter.kt @@ -15,5 +15,3 @@ fun Double.formatAvgRatingWithStar(): String { "-" } } - - From c5327515c3fb926cd0c8bcee376487db5e75fb92 Mon Sep 17 00:00:00 2001 From: lilin Date: Sun, 25 Jan 2026 23:00:13 +0900 Subject: [PATCH 12/12] =?UTF-8?q?UnitTest=E4=BF=AE=E6=AD=A3=E3=81=A8?= =?UTF-8?q?=E8=89=B2=E3=81=AE=E5=A4=89=E6=9B=B4=E3=81=A8stringResource?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/favorite/expanded/SortSection.kt | 3 +-- .../favorite/expanded/StatisticsSection.kt | 13 +++++++------ app/src/main/res/values/strings.xml | 4 ++++ .../feature/favorite/FavoriteViewModelTest.kt | 10 +++++----- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt index 896e7d1..6f46f1d 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/SortSection.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.lilin.gamelibrary.R import com.lilin.gamelibrary.domain.model.SortOption -import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd import com.lilin.gamelibrary.ui.theme.GameLibraryTheme @Composable @@ -66,7 +65,7 @@ private fun SortOptionItem( selected = isSelected, onClick = null, colors = RadioButtonDefaults.colors( - selectedColor = FavoriteGradientEnd, + selectedColor = MaterialTheme.colorScheme.primary, ), ) diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt index fb4bcc6..51a5233 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/expanded/StatisticsSection.kt @@ -11,11 +11,12 @@ import androidx.compose.material3.MaterialTheme 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.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.lilin.gamelibrary.R import com.lilin.gamelibrary.domain.model.FavoriteStatistics -import com.lilin.gamelibrary.ui.theme.FavoriteGradientEnd import com.lilin.gamelibrary.ui.theme.GameLibraryTheme import com.lilin.gamelibrary.ui.util.formatAvgRatingWithStar @@ -28,7 +29,7 @@ fun StatisticsSection( modifier = modifier, ) { Text( - text = "統計", + text = stringResource(R.string.favorite_statistics), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, @@ -36,19 +37,19 @@ fun StatisticsSection( Spacer(modifier = Modifier.height(12.dp)) StatisticItem( - label = "合計", + label = stringResource(R.string.favorite_statistics_total), value = statistics.totalCount.toString(), ) Spacer(modifier = Modifier.height(8.dp)) StatisticItem( - label = "平均評価", + label = stringResource(R.string.favorite_statistics_average), value = statistics.avgRating.formatAvgRatingWithStar(), ) Spacer(modifier = Modifier.height(8.dp)) StatisticItem( - label = "最新追加", + label = stringResource(R.string.favorite_statistics_latest), value = statistics.latestAdded, ) } @@ -73,7 +74,7 @@ private fun StatisticItem( text = value, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, - color = FavoriteGradientEnd, + color = MaterialTheme.colorScheme.primary, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb6642c..84b988f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,4 +84,8 @@ 「%s」をお気に入りから削除しますか? キャンセル 削除 + 統計 + 合計 + 平均評価 + 最新追加 diff --git a/app/src/test/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModelTest.kt b/app/src/test/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModelTest.kt index 727687f..f5d6ee9 100644 --- a/app/src/test/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModelTest.kt +++ b/app/src/test/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteViewModelTest.kt @@ -65,8 +65,8 @@ class FavoriteViewModelTest { ), ) - every { getFavoriteGamesUseCase(SortOrder.NEWEST_FIRST) } returns flowOf(testGames) - every { getFavoriteGamesUseCase(SortOrder.OLDEST_FIRST) } returns flowOf(testGames.reversed()) + every { getFavoriteGamesUseCase.invoke(SortOrder.NEWEST_FIRST) } returns flowOf(testGames) + every { getFavoriteGamesUseCase.invoke(SortOrder.OLDEST_FIRST) } returns flowOf(testGames.reversed()) viewModel = FavoriteViewModel(getFavoriteGamesUseCase, removeFavoriteGameUseCase) @@ -113,8 +113,8 @@ class FavoriteViewModelTest { ), ) - every { getFavoriteGamesUseCase(SortOrder.NEWEST_FIRST) } returns flowOf(testGames) - every { getFavoriteGamesUseCase(SortOrder.OLDEST_FIRST) } returns flowOf(testGames) + every { getFavoriteGamesUseCase.invoke(SortOrder.NEWEST_FIRST) } returns flowOf(testGames) + every { getFavoriteGamesUseCase.invoke(SortOrder.OLDEST_FIRST) } returns flowOf(testGames) viewModel = FavoriteViewModel(getFavoriteGamesUseCase, removeFavoriteGameUseCase) advanceUntilIdle() @@ -148,7 +148,7 @@ class FavoriteViewModelTest { @Test fun toggleSortOrder_withNonSuccessState_stateRemainsUnchanged() = runTest { - every { getFavoriteGamesUseCase(SortOrder.NEWEST_FIRST) } returns flowOf(emptyList()) + every { getFavoriteGamesUseCase.invoke(SortOrder.NEWEST_FIRST) } returns flowOf(emptyList()) viewModel = FavoriteViewModel(getFavoriteGamesUseCase, removeFavoriteGameUseCase)