[dev QoL] Added AndroidStudio previews for [presentation.history] namespace (#10012)

* Added display preview for HistoryDialogs

* Added preview with provider for each branch for HistoryItem

* Added previews for HistoryScreen

Created in-memory preferences construct for when its needed at top-level injection

* Fixed ktlint violations
This commit is contained in:
Caleb Morris 2023-10-14 19:23:11 -07:00 committed by GitHub
parent 0be7ac5871
commit 447bcb28ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 348 additions and 1 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
@ -18,13 +19,16 @@ import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@ -37,6 +41,7 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) {
Scaffold(
topBar = { scrollBehavior ->
@ -82,6 +87,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
)
}
}
@ -95,7 +101,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(),
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
@ -141,3 +147,32 @@ sealed interface HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel
data class Item(val item: HistoryWithRelations) : HistoryUiModel
}
@ThemePreviews
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State,
) {
TachiyomiTheme {
HistoryScreen(
state = historyState,
snackbarHostState = SnackbarHostState(),
onSearchQueryChange = {},
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View file

@ -0,0 +1,109 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenModel.State> {
private val multiPage = HistoryScreenModel.State(
searchQuery = null,
list =
listOf(HistoryUiModelExamples.headerToday)
.asSequence()
.plus(HistoryUiModelExamples.items().take(3))
.plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) })
.plus(HistoryUiModelExamples.items().take(1))
.plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) })
.plus(HistoryUiModelExamples.items().take(7))
.toList(),
dialog = null,
)
private val shortRecent = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val shortFuture = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val empty = HistoryScreenModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = HistoryScreenModel.State(
searchQuery = "Example Search Query",
)
private val loading = HistoryScreenModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<HistoryScreenModel.State> = sequenceOf(
multiPage,
shortRecent,
shortFuture,
empty,
loadingWithSearchQuery,
loading,
)
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
fun items() = sequence {
var count = 1
while (true) {
yield(randItem { it.copy(title = "Example Title $count") })
count += 1
}
}
fun randItem(historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it }) =
HistoryUiModel.Item(
historyBuilder(
HistoryWithRelations(
id = Random.nextLong(),
chapterId = Random.nextLong(),
mangaId = Random.nextLong(),
title = "Test Title",
chapterNumber = Random.nextDouble(),
readAt = Date.from(Instant.now()),
readDuration = Random.nextLong(),
coverData = MangaCover(
mangaId = Random.nextLong(),
sourceId = Random.nextLong(),
isMangaFavorite = Random.nextBoolean(),
url = "https://example.com/cover.png",
lastModified = Random.nextLong(),
),
),
),
)
}
}

View file

@ -0,0 +1,13 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.time.Instant
import java.util.Date
object HistoryUiModelProviders {
class HeadNow : PreviewParameterProvider<HistoryUiModel> {
override val values: Sequence<HistoryUiModel> =
sequenceOf(HistoryUiModel.Header(Date.from(Instant.now())))
}
}

View file

@ -18,7 +18,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun HistoryDeleteDialog(
@ -101,3 +103,14 @@ fun HistoryDeleteAllDialog(
},
)
}
@ThemePreviews
@Composable
internal fun HistoryDeleteDialogPreview() {
TachiyomiTheme {
HistoryDeleteDialog(
onDismissRequest = {},
onDelete = { _ -> run {} },
)
}
}

View file

@ -19,13 +19,16 @@ import androidx.compose.ui.Modifier
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.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val HISTORY_ITEM_HEIGHT = 96.dp
@ -87,3 +90,19 @@ fun HistoryItem(
}
}
}
@ThemePreviews
@Composable
internal fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations,
) {
TachiyomiTheme {
HistoryItem(
history = historyWithRelations,
onClickCover = {},
onClickResume = {},
onClickDelete = {},
)
}
}

View file

@ -0,0 +1,62 @@
package eu.kanade.presentation.history.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import tachiyomi.domain.history.model.HistoryWithRelations
import java.util.Date
internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWithRelations> {
private val simple = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = 10.2,
readAt = Date(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithoutReadAt = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = 10.2,
readAt = null,
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithNegativeChapterNumber = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = -2.0,
readAt = Date(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
override val values: Sequence<HistoryWithRelations>
get() = sequenceOf(simple, historyWithoutReadAt, historyWithNegativeChapterNumber)
}

View file

@ -0,0 +1,96 @@
package tachiyomi.core.preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
/**
* Local-copy implementation of PreferenceStore mostly for test and preview purposes
*/
class InMemoryPreferenceStore(
private val initialPreferences: Sequence<InMemoryPreference<*>> = sequenceOf(),
) : PreferenceStore {
private val preferences: Map<String, Preference<*>> =
initialPreferences.toList().associateBy { it.key() }
override fun getString(key: String, defaultValue: String): Preference<String> {
val default = InMemoryPreference(key, null, defaultValue)
val data: String? = preferences[key]?.get() as? String
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
val default = InMemoryPreference(key, null, defaultValue)
val data: Long? = preferences[key]?.get() as? Long
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
val default = InMemoryPreference(key, null, defaultValue)
val data: Int? = preferences[key]?.get() as? Int
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
val default = InMemoryPreference(key, null, defaultValue)
val data: Float? = preferences[key]?.get() as? Float
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
val default = InMemoryPreference(key, null, defaultValue)
val data: Boolean? = preferences[key]?.get() as? Boolean
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
TODO("Not yet implemented")
}
override fun <T> getObject(
key: String,
defaultValue: T,
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T> {
val default = InMemoryPreference(key, null, defaultValue)
val data: T? = preferences[key]?.get() as? T
return if (data == null) default else InMemoryPreference<T>(key, data, defaultValue)
}
override fun getAll(): Map<String, *> {
return preferences
}
class InMemoryPreference<T>(
private val key: String,
private var data: T?,
private val defaultValue: T,
) : Preference<T> {
override fun key(): String = key
override fun get(): T = data ?: defaultValue()
override fun isSet(): Boolean = data != null
override fun delete() {
data = null
}
override fun defaultValue(): T = defaultValue
override fun changes(): Flow<T> = flow { data }
override fun stateIn(scope: CoroutineScope): StateFlow<T> {
return changes().stateIn(scope, SharingStarted.Eagerly, get())
}
override fun set(value: T) {
data = value
}
}
}