[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:
parent
0be7ac5871
commit
447bcb28ef
7 changed files with 348 additions and 1 deletions
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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())))
|
||||
}
|
||||
}
|
|
@ -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 {} },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue