Last commit merged: 489d22720a
This commit is contained in:
LuftVerbot 2023-11-24 23:06:00 +01:00
parent 76df725cab
commit 150d43e325
98 changed files with 1613 additions and 1142 deletions

View file

@ -5,11 +5,6 @@
],
"schedule": ["every sunday"],
"packageRules": [
{
"managers": ["maven"],
"packageNames": ["com.google.guava:guava"],
"versionScheme": "docker"
},
{
// Compiler plugins are tightly coupled to Kotlin version
"groupName": "Kotlin",

View file

@ -24,6 +24,10 @@ Before you start, please note that the ability to use following technologies is
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help
- Join [the Discord server](https://discord.gg/F32UjdJZrR) for online help and to ask questions while developing.

3
app/.gitignore vendored
View file

@ -1,4 +1,3 @@
/build
*iml
*.iml
custom.gradle
*.iml

View file

@ -200,7 +200,7 @@ dependencies {
implementation(androidx.bundles.lifecycle)
// Job scheduling
implementation(androidx.bundles.workmanager)
implementation(androidx.workmanager)
// RxJava
implementation(libs.rxjava)

View file

@ -1,5 +1,4 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@drawable/sc_collections_bookmark_48dp"

View file

@ -2,6 +2,7 @@ package eu.kanade.domain.track.anime.interactor
import android.content.Context
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.domain.track.anime.model.toDomainTrack
import eu.kanade.domain.track.anime.service.DelayedAnimeTrackingUpdateJob
import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
@ -29,24 +30,26 @@ class TrackEpisode(
if (tracks.isEmpty()) return@launchNonCancellable
tracks.mapNotNull { track ->
val tracker = trackerManager.get(track.syncId)
if (tracker != null && tracker.isLoggedIn && episodeNumber > track.lastEpisodeSeen) {
val updatedTrack = track.copy(lastEpisodeSeen = episodeNumber)
val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
return@mapNotNull null
}
async {
runCatching {
if (context.isOnline()) {
tracker.animeService.update(updatedTrack.toDbTrack(), true)
val updatedTrack = service.animeService.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastEpisodeSeen = episodeNumber)
service.animeService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
delayedTrackingStore.removeAnimeItem(track.id)
} else {
delayedTrackingStore.addAnimeItem(updatedTrack)
delayedTrackingStore.addAnime(track.id, episodeNumber)
DelayedAnimeTrackingUpdateJob.setupTask(context)
}
}
}
} else {
null
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }

View file

@ -8,6 +8,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import eu.kanade.domain.track.anime.interactor.TrackEpisode
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
@ -22,7 +23,7 @@ import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
class DelayedAnimeTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
@ -31,9 +32,8 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
val getTracks = Injekt.get<GetAnimeTracks>()
val insertTrack = Injekt.get<InsertAnimeTrack>()
val trackEpisode = Injekt.get<TrackEpisode>()
val trackerManager = Injekt.get<TrackerManager>()
val delayedTrackingStore = Injekt.get<DelayedAnimeTrackingStore>()
withIOContext {
@ -46,19 +46,8 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble())
}
.forEach { animeTrack ->
try {
val tracker = trackerManager.get(animeTrack.syncId)
if (tracker != null && tracker.isLoggedIn) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${animeTrack.id}, last episode seen: ${animeTrack.lastEpisodeSeen}" }
tracker.animeService.update(animeTrack.toDbTrack(), true)
insertTrack.await(animeTrack)
}
delayedTrackingStore.removeAnimeItem(animeTrack.id)
null
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${animeTrack.animeId}, last chapter read: ${animeTrack.lastEpisodeSeen}" }
trackEpisode.await(context, animeTrack.animeId, animeTrack.lastEpisodeSeen)
}
}

View file

@ -13,13 +13,12 @@ class DelayedAnimeTrackingStore(context: Context) {
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addAnimeItem(track: AnimeTrack) {
val trackId = track.id.toString()
val lastEpisodeSeen = preferences.getFloat(trackId, 0f)
if (track.lastEpisodeSeen > lastEpisodeSeen) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: ${track.lastEpisodeSeen}" }
fun addAnime(trackId: Long, lastEpisodeSeen: Double) {
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
if (lastEpisodeSeen > previousLastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: $lastEpisodeSeen" }
preferences.edit {
putFloat(trackId, track.lastEpisodeSeen.toFloat())
putFloat(trackId.toString(), lastEpisodeSeen.toFloat())
}
}
}

View file

@ -2,6 +2,7 @@ package eu.kanade.domain.track.manga.interactor
import android.content.Context
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.domain.track.manga.model.toDomainTrack
import eu.kanade.domain.track.manga.service.DelayedMangaTrackingUpdateJob
import eu.kanade.domain.track.manga.store.DelayedMangaTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
@ -27,25 +28,29 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val tracker = trackerManager.get(track.syncId)
if (tracker != null && tracker.isLoggedIn && chapterNumber > track.lastChapterRead) {
val updatedTrack = track.copy(lastChapterRead = chapterNumber)
val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null
}
}
async {
runCatching {
try {
tracker.mangaService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
} catch (e: Exception) {
delayedTrackingStore.addMangaItem(updatedTrack)
DelayedMangaTrackingUpdateJob.setupTask(context)
throw e
}
async {
runCatching {
try {
val updatedTrack = service.mangaService.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastChapterRead = chapterNumber)
service.mangaService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
delayedTrackingStore.removeMangaItem(track.id)
} catch (e: Exception) {
delayedTrackingStore.addManga(track.id, chapterNumber)
DelayedMangaTrackingUpdateJob.setupTask(context)
throw e
}
}
}
} else {
null
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }

View file

@ -8,6 +8,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import eu.kanade.domain.track.manga.interactor.TrackChapter
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.domain.track.manga.store.DelayedMangaTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
@ -22,7 +23,7 @@ import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
class DelayedMangaTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
@ -31,9 +32,8 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
val getTracks = Injekt.get<GetMangaTracks>()
val insertTrack = Injekt.get<InsertMangaTrack>()
val trackChapter = Injekt.get<TrackChapter>()
val trackerManager = Injekt.get<TrackerManager>()
val delayedTrackingStore = Injekt.get<DelayedMangaTrackingStore>()
withIOContext {
@ -46,19 +46,8 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.forEach { track ->
try {
val tracker = trackerManager.get(track.syncId)
if (tracker != null && tracker.isLoggedIn) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
tracker.mangaService.update(track.toDbTrack(), true)
insertTrack.await(track)
}
delayedTrackingStore.removeMangaItem(track.id)
null
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
trackChapter.await(context, track.mangaId, track.lastChapterRead)
}
}

View file

@ -13,13 +13,12 @@ class DelayedMangaTrackingStore(context: Context) {
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addMangaItem(track: MangaTrack) {
val trackId = track.id.toString()
val lastChapterRead = preferences.getFloat(trackId, 0f)
if (track.lastChapterRead > lastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
fun addManga(trackId: Long, lastChapterRead: Double) {
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
if (lastChapterRead > previousLastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
preferences.edit {
putFloat(trackId, track.lastChapterRead.toFloat())
putFloat(trackId.toString(),lastChapterRead.toFloat())
}
}
}

View file

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
@ -65,6 +66,7 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Text
title: String?,
subtitle: String? = null,
@ -86,6 +88,7 @@ fun AppBar(
AppBar(
modifier = modifier,
backgroundColor = backgroundColor,
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
@ -111,6 +114,7 @@ fun AppBar(
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Title
titleContent: @Composable () -> Unit,
// Up button
@ -147,7 +151,7 @@ fun AppBar(
title = titleContent,
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp,
),
),
@ -193,6 +197,9 @@ fun AppBarTitle(
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee(
delayMillis = 2_000,
),
)
}
}

View file

@ -29,6 +29,7 @@ import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
@ -184,25 +185,16 @@ private fun SetAsDefaultDialog(
) {
Text(text = stringResource(R.string.confirm_set_chapter_settings))
Row(
modifier = Modifier
.clickable { optionalChecked = !optionalChecked }
.padding(vertical = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = optionalChecked,
onCheckedChange = null,
)
Text(text = stringResource(R.string.also_set_episode_settings_for_library))
}
LabeledCheckbox(
label = stringResource(R.string.also_set_episode_settings_for_library),
checked = optionalChecked,
onCheckedChange = { optionalChecked = it },
)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View file

@ -63,6 +63,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -605,73 +606,70 @@ private fun AnimeSummary(
expanded: Boolean,
modifier: Modifier = Modifier,
) {
var expandedHeight by remember { mutableIntStateOf(0) }
var shrunkHeight by remember { mutableIntStateOf(0) }
val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
val shrunkPlaceable = subcompose("description-s") {
Text(
text = "\n\n", // Shows at least 3 lines
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
val expandedPlaceable = subcompose("description-l") {
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(
shrunkHeight,
) ?: 0
val actualPlaceable = subcompose("description") {
SelectionContainer {
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
{
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
text = "\n\n", // Shows at least 3 lines
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
}.map { it.measure(constraints) }
},
{
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
},
{
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
},
{
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
Box(
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
)
}
},
),
) { (shrunk, expanded, actual, scrim), constraints ->
val shrunkHeight = shrunk.single()
.measure(constraints)
.height
val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx()
val scrimPlaceable = subcompose("scrim") {
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
Box(
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(
if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand,
),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(
Brush.radialGradient(colors = colors.asReversed()),
),
)
}
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
val actualPlaceable = actual.single()
.measure(constraints)
val scrimPlaceable = scrim.single()
.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight))
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
layout(constraints.maxWidth, currentHeight) {
actualPlaceable.forEach {
it.place(0, 0)
}
actualPlaceable.place(0, 0)
val scrimY = currentHeight - scrimHeight
scrimPlaceable.forEach {
it.place(0, scrimY)
}
scrimPlaceable.place(0, scrimY)
}
}
}

View file

@ -29,6 +29,7 @@ import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
@ -184,25 +185,16 @@ private fun SetAsDefaultDialog(
) {
Text(text = stringResource(R.string.confirm_set_chapter_settings))
Row(
modifier = Modifier
.clickable { optionalChecked = !optionalChecked }
.padding(vertical = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = optionalChecked,
onCheckedChange = null,
)
Text(text = stringResource(R.string.also_set_chapter_settings_for_library))
}
LabeledCheckbox(
label = stringResource(R.string.also_set_chapter_settings_for_library),
checked = optionalChecked,
onCheckedChange = { optionalChecked = it },
)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View file

@ -63,6 +63,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -605,73 +606,70 @@ private fun MangaSummary(
expanded: Boolean,
modifier: Modifier = Modifier,
) {
var expandedHeight by remember { mutableIntStateOf(0) }
var shrunkHeight by remember { mutableIntStateOf(0) }
val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
val shrunkPlaceable = subcompose("description-s") {
Text(
text = "\n\n", // Shows at least 3 lines
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
val expandedPlaceable = subcompose("description-l") {
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(
shrunkHeight,
) ?: 0
val actualPlaceable = subcompose("description") {
SelectionContainer {
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
{
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
text = "\n\n", // Shows at least 3 lines
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
}.map { it.measure(constraints) }
},
{
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
},
{
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
},
{
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
Box(
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
)
}
},
),
) { (shrunk, expanded, actual, scrim), constraints ->
val shrunkHeight = shrunk.single()
.measure(constraints)
.height
val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx()
val scrimPlaceable = subcompose("scrim") {
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
Box(
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(
if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand,
),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(
Brush.radialGradient(colors = colors.asReversed()),
),
)
}
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
val actualPlaceable = actual.single()
.measure(constraints)
val scrimPlaceable = scrim.single()
.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight))
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
layout(constraints.maxWidth, currentHeight) {
actualPlaceable.forEach {
it.place(0, 0)
}
actualPlaceable.place(0, 0)
val scrimY = currentHeight - scrimHeight
scrimPlaceable.forEach {
it.place(0, scrimY)
}
scrimPlaceable.place(0, scrimY)
}
}
}

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.history
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -18,7 +19,11 @@ 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.components.LabeledCheckbox
import tachiyomi.presentation.core.util.ThemePreviews
import kotlin.random.Random
@Composable
fun HistoryDeleteDialog(
@ -33,30 +38,17 @@ fun HistoryDeleteDialog(
Text(text = stringResource(R.string.action_remove))
},
text = {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
val subtitle = if (isManga) R.string.dialog_with_checkbox_remove_description else R.string.dialog_with_checkbox_remove_description_anime
Text(text = stringResource(subtitle))
Row(
modifier = Modifier
.padding(top = 16.dp)
.toggleable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
value = removeEverything,
onValueChange = { removeEverything = it },
),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = removeEverything,
onCheckedChange = null,
)
val subtext = if (isManga) R.string.dialog_with_checkbox_reset else R.string.dialog_with_checkbox_reset_anime
Text(
modifier = Modifier.padding(start = 4.dp),
text = stringResource(subtext),
)
}
LabeledCheckbox(
label = stringResource(R.string.dialog_with_checkbox_reset),
checked = removeEverything,
onCheckedChange = { removeEverything = it },
)
}
},
onDismissRequest = onDismissRequest,
@ -104,3 +96,15 @@ fun HistoryDeleteAllDialog(
},
)
}
@ThemePreviews
@Composable
private fun HistoryDeleteDialogPreview() {
TachiyomiTheme {
HistoryDeleteDialog(
onDismissRequest = {},
onDelete = {},
isManga = Random.nextBoolean(),
)
}
}

View file

@ -1,4 +1,4 @@
package eu.kanade.presentation.animehistory.components
package eu.kanade.presentation.history.anime
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.items
@ -7,12 +7,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.history.anime.AnimeHistoryItem
import eu.kanade.presentation.history.anime.AnimeHistoryUiModel
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun AnimeHistoryContent(
@ -21,7 +17,7 @@ fun AnimeHistoryContent(
onClickCover: (AnimeHistoryWithRelations) -> Unit,
onClickResume: (AnimeHistoryWithRelations) -> Unit,
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(),
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }

View file

@ -19,15 +19,20 @@ 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.entries.ItemCover
import eu.kanade.presentation.history.manga.MangaHistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.formatEpisodeNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val HISTORY_ITEM_HEIGHT = 96.dp
private val HistoryItemHeight = 96.dp
@Composable
fun AnimeHistoryItem(
@ -40,7 +45,7 @@ fun AnimeHistoryItem(
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(HISTORY_ITEM_HEIGHT)
.height(HistoryItemHeight)
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
@ -90,3 +95,19 @@ fun AnimeHistoryItem(
}
}
}
@ThemePreviews
@Composable
private fun HistoryItemPreviews(
@PreviewParameter(AnimeHistoryWithRelationsProvider::class)
historyWithRelations: AnimeHistoryWithRelations,
) {
TachiyomiTheme {
AnimeHistoryItem(
history = historyWithRelations,
onClickCover = {},
onClickResume = {},
onClickDelete = {},
)
}
}

View file

@ -6,13 +6,18 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.animehistory.components.AnimeHistoryContent
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import java.util.Date
@Composable
@ -24,6 +29,7 @@ fun AnimeHistoryScreen(
onClickCover: (animeId: Long) -> Unit,
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences,
) {
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
@ -52,6 +58,7 @@ fun AnimeHistoryScreen(
AnimeHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
)
}
}
@ -62,3 +69,33 @@ sealed interface AnimeHistoryUiModel {
data class Header(val date: Date) : AnimeHistoryUiModel
data class Item(val item: AnimeHistoryWithRelations) : AnimeHistoryUiModel
}
@ThemePreviews
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(AnimeHistoryScreenModelStateProvider::class)
historyState: AnimeHistoryScreenModel.State,
) {
TachiyomiTheme {
AnimeHistoryScreen(
state = historyState,
contentPadding = topSmallPaddingValues,
snackbarHostState = SnackbarHostState(),
searchQuery = null,
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View file

@ -0,0 +1,108 @@
package eu.kanade.presentation.history.anime
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
class AnimeHistoryScreenModelStateProvider : PreviewParameterProvider<AnimeHistoryScreenModel.State> {
private val multiPage = AnimeHistoryScreenModel.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 = AnimeHistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val shortFuture = AnimeHistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val empty = AnimeHistoryScreenModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = AnimeHistoryScreenModel.State(
searchQuery = "Example Search Query",
)
private val loading = AnimeHistoryScreenModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<AnimeHistoryScreenModel.State> = sequenceOf(
multiPage,
shortRecent,
shortFuture,
empty,
loadingWithSearchQuery,
loading,
)
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
AnimeHistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
AnimeHistoryUiModel.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: (AnimeHistoryWithRelations) -> AnimeHistoryWithRelations = { it }) =
AnimeHistoryUiModel.Item(
historyBuilder(
AnimeHistoryWithRelations(
id = Random.nextLong(),
episodeId = Random.nextLong(),
animeId = Random.nextLong(),
title = "Test Title",
episodeNumber = Random.nextDouble(),
seenAt = Date.from(Instant.now()),
coverData = AnimeCover(
animeId = Random.nextLong(),
sourceId = Random.nextLong(),
isAnimeFavorite = Random.nextBoolean(),
url = "https://example.com/cover.png",
lastModified = Random.nextLong(),
),
),
),
)
}
}

View file

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

View file

@ -0,0 +1,59 @@
package eu.kanade.presentation.history.anime
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import java.util.Date
internal class AnimeHistoryWithRelationsProvider : PreviewParameterProvider<AnimeHistoryWithRelations> {
private val simple = AnimeHistoryWithRelations(
id = 1L,
episodeId = 2L,
animeId = 3L,
title = "Test Title",
episodeNumber = 10.2,
seenAt = Date(1697247357L),
coverData = tachiyomi.domain.entries.anime.model.AnimeCover(
animeId = 3L,
sourceId = 4L,
isAnimeFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithoutReadAt = AnimeHistoryWithRelations(
id = 1L,
episodeId = 2L,
animeId = 3L,
title = "Test Title",
episodeNumber = 10.2,
seenAt = null,
coverData = tachiyomi.domain.entries.anime.model.AnimeCover(
animeId = 3L,
sourceId = 4L,
isAnimeFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithNegativeChapterNumber = AnimeHistoryWithRelations(
id = 1L,
episodeId = 2L,
animeId = 3L,
title = "Test Title",
episodeNumber = -2.0,
seenAt = Date(1697247357L),
coverData = tachiyomi.domain.entries.anime.model.AnimeCover(
animeId = 3L,
sourceId = 4L,
isAnimeFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
override val values: Sequence<AnimeHistoryWithRelations>
get() = sequenceOf(simple, historyWithoutReadAt, historyWithNegativeChapterNumber)
}

View file

@ -2,13 +2,19 @@ package eu.kanade.presentation.history.manga
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -19,7 +25,7 @@ fun MangaHistoryContent(
onClickCover: (MangaHistoryWithRelations) -> Unit,
onClickResume: (MangaHistoryWithRelations) -> Unit,
onClickDelete: (MangaHistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(),
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }

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.entries.ItemCover
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.manga.model.MangaHistoryWithRelations
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val HISTORY_ITEM_HEIGHT = 96.dp
@ -90,3 +93,19 @@ fun MangaHistoryItem(
}
}
}
@ThemePreviews
@Composable
internal fun HistoryItemPreviews(
@PreviewParameter(MangaHistoryWithRelationsProvider::class)
historyWithRelations: MangaHistoryWithRelations,
) {
TachiyomiTheme {
MangaHistoryItem(
history = historyWithRelations,
onClickCover = {},
onClickResume = {},
onClickDelete = {},
)
}
}

View file

@ -6,12 +6,20 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
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
@Composable
@ -23,6 +31,7 @@ fun MangaHistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences,
) {
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
@ -51,6 +60,7 @@ fun MangaHistoryScreen(
MangaHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
)
}
}
@ -61,3 +71,33 @@ sealed interface MangaHistoryUiModel {
data class Header(val date: Date) : MangaHistoryUiModel
data class Item(val item: MangaHistoryWithRelations) : MangaHistoryUiModel
}
@ThemePreviews
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(MangaHistoryScreenModelStateProvider::class)
historyState: MangaHistoryScreenModel.State,
) {
TachiyomiTheme {
MangaHistoryScreen(
state = historyState,
contentPadding = topSmallPaddingValues,
snackbarHostState = SnackbarHostState(),
searchQuery = null,
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.manga
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import tachiyomi.domain.entries.manga.model.MangaCover
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
class MangaHistoryScreenModelStateProvider: PreviewParameterProvider<MangaHistoryScreenModel.State> {
private val multiPage = MangaHistoryScreenModel.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 = MangaHistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val shortFuture = MangaHistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val empty = MangaHistoryScreenModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = MangaHistoryScreenModel.State(
searchQuery = "Example Search Query",
)
private val loading = MangaHistoryScreenModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<MangaHistoryScreenModel.State> = sequenceOf(
multiPage,
shortRecent,
shortFuture,
empty,
loadingWithSearchQuery,
loading,
)
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
MangaHistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
MangaHistoryUiModel.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: (MangaHistoryWithRelations) -> MangaHistoryWithRelations = { it }) =
MangaHistoryUiModel.Item(
historyBuilder(
MangaHistoryWithRelations(
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.manga
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.time.Instant
import java.util.Date
object MangaHistoryUiModelProviders {
class HeadNow : PreviewParameterProvider<MangaHistoryUiModel> {
override val values: Sequence<MangaHistoryUiModel> =
sequenceOf(MangaHistoryUiModel.Header(Date.from(Instant.now())))
}
}

View file

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

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.CheckboxState
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable
fun DeleteLibraryEntryDialog(
@ -65,27 +66,18 @@ fun DeleteLibraryEntryDialog(
text = {
Column {
list.forEach { state ->
val onCheck = {
val index = list.indexOf(state)
if (index != -1) {
val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onCheck() },
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = state.isChecked,
onCheckedChange = { onCheck() },
)
Text(text = stringResource(state.value))
}
LabeledCheckbox(
label = stringResource(state.value),
checked = state.isChecked,
onCheckedChange = {
val index = list.indexOf(state)
if (index != -1) {
val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
},
)
}
}
},

View file

@ -7,6 +7,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.UpIcon
import tachiyomi.presentation.core.components.material.Scaffold
@ -19,15 +20,9 @@ fun PreferenceScaffold(
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(titleRes)) },
navigationIcon = {
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
UpIcon()
}
}
},
AppBar(
title = stringResource(titleRes),
navigateUp = onBackPressed,
actions = actions,
scrollBehavior = it,
)

View file

@ -371,21 +371,6 @@ object SettingsAdvancedScreen : SearchableSettings {
)
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_refresh_library_tracking),
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
enabled = trackerManager.hasLoggedIn(),
onClick = {
MangaLibraryUpdateJob.startNow(
context,
target = MangaLibraryUpdateJob.Target.TRACKING,
)
AnimeLibraryUpdateJob.startNow(
context,
target = AnimeLibraryUpdateJob.Target.TRACKING,
)
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_reset_viewer_flags),
subtitle = stringResource(R.string.pref_reset_viewer_flags_summary),

View file

@ -62,6 +62,7 @@ import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
import tachiyomi.domain.backup.service.FLAG_HISTORY
import tachiyomi.domain.backup.service.FLAG_SETTINGS
import tachiyomi.domain.backup.service.FLAG_TRACK
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.isScrolledToEnd
@ -175,22 +176,23 @@ object SettingsBackupScreen : SearchableSettings {
val state = rememberLazyListState()
ScrollbarLazyColumn(state = state) {
item {
CreateBackupDialogItem(
isSelected = true,
title = stringResource(R.string.entries),
LabeledCheckbox(
label = stringResource(R.string.entries),
checked = true,
onCheckedChange = {},
)
}
choices.forEach { (k, v) ->
item {
val isSelected = flags.contains(k)
CreateBackupDialogItem(
isSelected = isSelected,
title = stringResource(v),
modifier = Modifier.clickable {
if (isSelected) {
flags.remove(k)
} else {
LabeledCheckbox(
label = stringResource(v),
checked = isSelected,
onCheckedChange = {
if (it) {
flags.add(k)
} else {
flags.remove(k)
}
},
)
@ -227,29 +229,6 @@ object SettingsBackupScreen : SearchableSettings {
)
}
@Composable
private fun CreateBackupDialogItem(
modifier: Modifier = Modifier,
isSelected: Boolean,
title: String,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.fillMaxWidth(),
) {
Checkbox(
modifier = Modifier.heightIn(min = 48.dp),
checked = isSelected,
onCheckedChange = null,
)
Text(
text = title,
style = MaterialTheme.typography.bodyMedium.merge(),
modifier = Modifier.padding(start = 24.dp),
)
}
}
@Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
@ -261,7 +240,7 @@ object SettingsBackupScreen : SearchableSettings {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.invalid_backup_file)) },
text = { Text(text = "${err.uri}\n\n${err.message}") },
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
@ -269,7 +248,7 @@ object SettingsBackupScreen : SearchableSettings {
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.copy))
Text(text = stringResource(R.string.action_copy_to_clipboard))
}
},
confirmButton = {
@ -340,25 +319,24 @@ object SettingsBackupScreen : SearchableSettings {
}
},
) {
if (it != null) {
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(
it,
results.missingSources,
results.missingTrackers,
)
if (it == null) {
error = InvalidRestore(message = context.getString(R.string.file_null_uri_error))
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
}
return Preference.PreferenceItem.TextPreference(
@ -476,6 +454,6 @@ private data class MissingRestoreComponents(
)
private data class InvalidRestore(
val uri: Uri,
val uri: Uri? = null,
val message: String,
)

View file

@ -285,12 +285,6 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(R.string.pref_library_update_refresh_metadata),
subtitle = stringResource(R.string.pref_library_update_refresh_metadata_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateTrackers(),
enabled = Injekt.get<TrackerManager>().hasLoggedIn(),
title = stringResource(R.string.pref_library_update_refresh_trackers),
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateItemRestrictions(),
title = stringResource(R.string.pref_library_update_manga_restriction),

View file

@ -82,21 +82,13 @@ object SettingsMainScreen : Screen() {
val backPress = LocalBackPress.currentOrThrow
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
val topBarState = rememberTopAppBarState()
Scaffold(
topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState),
topBar = { scrollBehavior ->
TopAppBar(
title = {
Text(
text = stringResource(R.string.label_settings),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = backPress::invoke) {
UpIcon()
}
},
AppBar(
title = stringResource(R.string.label_settings),
navigateUp = backPress::invoke,
actions = {
AppBarActions(
listOf(

View file

@ -41,19 +41,9 @@ class OpenSourceLibraryLicenseScreen(
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
AppBar(
title = name,
navigateUp = navigator::pop,
actions = {
if (!website.isNullOrEmpty()) {
AppBarActions(

View file

@ -47,13 +47,9 @@ class BackupSchemaScreen : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
AppBar(
title = title,
navigateUp = navigator::pop,
actions = {
AppBarActions(
listOf(

View file

@ -16,6 +16,7 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.guava.await
class DebugInfoScreen : Screen() {
@ -68,15 +69,7 @@ class DebugInfoScreen : Screen() {
@Composable
@ReadOnlyComposable
private fun getWebViewVersion(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?"
val pm = LocalContext.current.packageManager
val label = webView.applicationInfo.loadLabel(pm)
val version = webView.versionName
return "$label $version"
} else {
return "Unknown"
}
return WebViewUtil.getVersion(LocalContext.current)
}
@Composable

View file

@ -61,13 +61,9 @@ class WorkerInfoScreen : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
AppBar(
title = title,
navigateUp = navigator::pop,
actions = {
AppBarActions(
listOf(

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable
fun MultiSelectListPreferenceWidget(
@ -55,33 +56,17 @@ fun MultiSelectListPreferenceWidget(
preference.entries.forEach { current ->
item {
val isSelected = selected.contains(current.key)
val onSelectionChanged = {
when (!isSelected) {
true -> selected.add(current.key)
false -> selected.remove(current.key)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
)
.minimumInteractiveComponentSize()
.fillMaxWidth(),
) {
Checkbox(
checked = isSelected,
onCheckedChange = null,
)
Text(
text = current.value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 24.dp),
)
}
LabeledCheckbox(
label = current.value,
checked = isSelected,
onCheckedChange = {
if (it) {
selected.add(current.key)
} else {
selected.remove(current.key)
}
},
)
}
}
}

View file

@ -152,7 +152,7 @@ private fun ItemDeleteDialog(
onDismissRequest()
},
content = {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
},
)
},

View file

@ -1,4 +1,4 @@
package eu.kanade.presentation.reader
package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@ -15,6 +15,7 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -24,6 +25,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
@ -32,11 +34,6 @@ fun BottomReaderBar(
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Row(
modifier = Modifier
.fillMaxWidth()

View file

@ -1,4 +1,4 @@
package eu.kanade.presentation.reader
package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource

View file

@ -0,0 +1,166 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
private val animationSpec = tween<IntOffset>(200)
@Composable
fun ReaderAppBars(
visible: Boolean,
fullscreen: Boolean,
mangaTitle: String?,
chapterTitle: String?,
navigateUp: () -> Unit,
onClickTopAppBar: () -> Unit,
bookmarked: Boolean,
onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onShare: (() -> Unit)?,
viewer: Viewer?,
onNextChapter: () -> Unit,
enabledNext: Boolean,
onPreviousChapter: () -> Unit,
enabledPrevious: Boolean,
currentPage: Int,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
val isRtl = viewer is R2LPagerViewer
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars)
} else {
Modifier
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = animationSpec,
),
) {
AppBar(
modifier = appBarModifier
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
listOfNotNull(
AppBar.Action(
title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark),
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
onClick = onToggleBookmarked,
),
onOpenInWebView?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_web_view),
onClick = it,
)
},
onShare?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = it,
)
},
),
)
},
)
}
Spacer(modifier = Modifier.weight(1f))
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = animationSpec,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = onNextChapter,
enabledNext = enabledNext,
onPreviousChapter = onPreviousChapter,
enabledPrevious = enabledPrevious,
currentPage = currentPage,
totalPages = totalPages,
onSliderValueChange = onSliderValueChange,
)
BottomReaderBar(
backgroundColor = backgroundColor,
readingMode = readingMode,
onClickReadingMode = onClickReadingMode,
orientationMode = orientationMode,
onClickOrientationMode = onClickOrientationMode,
cropEnabled = cropEnabled,
onClickCropBorder = onClickCropBorder,
onClickSettings = onClickSettings,
)
}
}
}
}

View file

@ -185,7 +185,7 @@ fun TrackDateSelector(
Spacer(modifier = Modifier.weight(1f))
}
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) {
Text(text = stringResource(R.string.action_ok))
@ -226,7 +226,7 @@ fun BaseSelector(
Spacer(modifier = Modifier.weight(1f))
}
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(onClick = onConfirm) {
Text(text = stringResource(R.string.action_ok))

View file

@ -7,6 +7,7 @@ import android.webkit.WebView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@ -14,6 +15,8 @@ import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -22,8 +25,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.LoadingState
import com.google.accompanist.web.WebView
@ -72,7 +77,7 @@ fun WebViewScreenContent(
super.onPageFinished(view, url)
scope.launch {
val html = view.getHtml()
showCloudflareHelp = "window._cf_chl_opt" in html
showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html
}
}
@ -103,54 +108,71 @@ fun WebViewScreenContent(
Scaffold(
topBar = {
Box {
AppBar(
title = state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
}
Column {
AppBar(
title = state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
}
},
enabled = navigator.canGoBack,
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
}
},
enabled = navigator.canGoForward,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_webview_refresh),
onClick = { navigator.reload() },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = { onShare(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_browser),
onClick = { onOpenInBrowser(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
)
},
)
if (showCloudflareHelp) {
Surface(
modifier = Modifier.padding(8.dp),
) {
WarningBanner(
textRes = R.string.information_cloudflare_help,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
},
enabled = navigator.canGoBack,
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
}
},
enabled = navigator.canGoForward,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_webview_refresh),
onClick = { navigator.reload() },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = { onShare(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_browser),
onClick = { onOpenInBrowser(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
)
},
)
)
}
}
}
when (val loadingState = state.loadingState) {
is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier
@ -168,40 +190,27 @@ fun WebViewScreenContent(
}
},
) { contentPadding ->
Column(
modifier = Modifier.padding(contentPadding),
) {
if (showCloudflareHelp) {
WarningBanner(
textRes = R.string.information_cloudflare_help,
modifier = Modifier.clickable {
uriHandler.openUri(
"https://aniyomi.org/docs/guides/troubleshooting/#cloudflare",
)
},
)
}
WebView(
state = state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
WebView(
state = state,
modifier = Modifier.weight(1f),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
}
}

View file

@ -364,11 +364,11 @@ class BackupCreator(
private fun backupExtensionPreferences(flags: Int): List<BackupExtensionPreferences> {
if (flags and BackupConst.BACKUP_EXT_PREFS_MASK != BackupConst.BACKUP_EXT_PREFS) return emptyList()
val prefs = mutableListOf<Pair<String, SharedPreferences>>()
Injekt.get<AnimeSourceManager>().getOnlineSources().forEach {
Injekt.get<AnimeSourceManager>().getCatalogueSources().forEach {
val name = it.getPreferenceKey()
prefs += Pair(name, context.getSharedPreferences(name, 0x0))
}
Injekt.get<MangaSourceManager>().getOnlineSources().forEach {
Injekt.get<MangaSourceManager>().getCatalogueSources().forEach {
val name = it.getPreferenceKey()
prefs += Pair(name, context.getSharedPreferences(name, 0x0))
}

View file

@ -30,6 +30,8 @@ import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.source.anime.model.copyFrom
import eu.kanade.tachiyomi.source.manga.model.copyFrom
import eu.kanade.tachiyomi.util.BackupUtil
@ -1203,6 +1205,10 @@ class BackupRestorer(
preferences: List<BackupPreference>,
sharedPrefs: SharedPreferences,
) {
MangaLibraryUpdateJob.setupTask(context)
AnimeLibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
preferences.forEach { pref ->
when (pref.value) {
is IntPreferenceValue -> {

View file

@ -89,7 +89,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
private val updateAnime: UpdateAnime = Injekt.get()
private val getCategories: GetAnimeCategories = Injekt.get()
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get()
private val refreshAnimeTracks: RefreshAnimeTracks = Injekt.get()
private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
private val notifier = AnimeLibraryUpdateNotifier(context)
@ -131,7 +130,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
when (target) {
Target.EPISODES -> updateEpisodeList()
Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
}
Result.success()
} catch (e: Exception) {
@ -304,10 +302,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
}
failedUpdates.add(anime to errorMessage)
}
if (libraryPreferences.autoUpdateTrackers().get()) {
refreshAnimeTracks(anime.id)
}
}
}
}
@ -413,37 +407,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
notifier.cancelProgressNotification()
}
/**
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private suspend fun updateTrackings() {
coroutineScope {
var progressCount = 0
animeToUpdate.forEach { libraryAnime ->
ensureActive()
val anime = libraryAnime.anime
notifier.showProgressNotification(
listOf(anime),
progressCount++,
animeToUpdate.size,
)
refreshAnimeTracks(anime.id)
}
notifier.cancelProgressNotification()
}
}
private suspend fun refreshAnimeTracks(animeId: Long) {
refreshAnimeTracks.await(animeId).forEach { (_, e) ->
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
private suspend fun withUpdateNotification(
updatingAnime: CopyOnWriteArrayList<Anime>,
completed: AtomicInteger,
@ -510,7 +473,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
enum class Target {
EPISODES, // Anime episodes
COVERS, // Anime covers
TRACKING, // Tracking metadata
}
companion object {

View file

@ -89,7 +89,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
private val updateManga: UpdateManga = Injekt.get()
private val getCategories: GetMangaCategories = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val refreshMangaTracks: RefreshMangaTracks = Injekt.get()
private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
private val notifier = MangaLibraryUpdateNotifier(context)
@ -131,7 +130,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
when (target) {
Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
}
Result.success()
} catch (e: Exception) {
@ -303,10 +301,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
}
failedUpdates.add(manga to errorMessage)
}
if (libraryPreferences.autoUpdateTrackers().get()) {
refreshMangaTracks(manga.id)
}
}
}
}
@ -412,37 +406,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
notifier.cancelProgressNotification()
}
/**
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private suspend fun updateTrackings() {
coroutineScope {
var progressCount = 0
mangaToUpdate.forEach { libraryManga ->
ensureActive()
val manga = libraryManga.manga
notifier.showProgressNotification(
listOf(manga),
progressCount++,
mangaToUpdate.size,
)
refreshMangaTracks(manga.id)
}
notifier.cancelProgressNotification()
}
}
private suspend fun refreshMangaTracks(mangaId: Long) {
refreshMangaTracks.await(mangaId).forEach { (_, e) ->
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
@ -509,7 +472,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
enum class Target {
CHAPTERS, // Manga chapters
COVERS, // Manga covers
TRACKING, // Tracking metadata
}
companion object {

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
@ -39,12 +40,12 @@ class ShikimoriApi(
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: MangaTrack, user_id: String): MangaTrack {
suspend fun addLibManga(track: MangaTrack, userId: String): MangaTrack {
return withIOContext {
with(json) {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("user_id", userId)
put("target_id", track.media_id)
put("target_type", "Manga")
put("chapters", track.last_chapter_read.toInt())
@ -67,9 +68,9 @@ class ShikimoriApi(
}
}
suspend fun updateLibManga(track: MangaTrack, user_id: String): MangaTrack = addLibManga(
suspend fun updateLibManga(track: MangaTrack, userId: String): MangaTrack = addLibManga(
track,
user_id,
userId,
)
suspend fun deleteLibManga(track: MangaTrack): MangaTrack {
@ -83,12 +84,12 @@ class ShikimoriApi(
}
}
suspend fun addLibAnime(track: AnimeTrack, user_id: String): AnimeTrack {
suspend fun addLibAnime(track: AnimeTrack, userId: String): AnimeTrack {
return withIOContext {
with(json) {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("user_id", userId)
put("target_id", track.media_id)
put("target_type", "Anime")
put("chapters", track.last_episode_seen.toInt())
@ -111,9 +112,9 @@ class ShikimoriApi(
}
}
suspend fun updateLibAnime(track: AnimeTrack, user_id: String): AnimeTrack = addLibAnime(
suspend fun updateLibAnime(track: AnimeTrack, userId: String): AnimeTrack = addLibAnime(
track,
user_id,
userId,
)
suspend fun deleteLibAnime(track: AnimeTrack): AnimeTrack {
@ -323,14 +324,14 @@ class ShikimoriApi(
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.me"
private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "$baseUrl/api"
private const val oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth"
fun authUrl() = loginUrl.toUri().buildUpon()
fun authUrl(): Uri = loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")

View file

@ -127,7 +127,7 @@ internal class AnimeExtensionGithubApi {
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.map(extensionAnimeSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@ -97,7 +98,8 @@ internal object AnimeExtensionLoader {
"${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION",
)
return try {
file.copyTo(target, overwrite = true)
target.delete()
file.copyAndSetReadOnlyTo(target, overwrite = true)
if (currentExtension != null) {
AnimeExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
} else {

View file

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@ -109,7 +110,8 @@ internal object MangaExtensionLoader {
"${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION",
)
return try {
file.copyTo(target, overwrite = true)
target.delete()
file.copyAndSetReadOnlyTo(target, overwrite = true)
if (currentExtension != null) {
MangaExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
} else {

View file

@ -34,6 +34,7 @@ import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
@ -55,17 +56,9 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = Injekt.get<AnimeSourceManager>().getOrStub(sourceId).toString(),
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
UpIcon()
}
},
AppBar(
title = Injekt.get<AnimeSourceManager>().getOrStub(sourceId).toString(),
navigateUp = navigator::pop,
scrollBehavior = it,
)
},

View file

@ -55,6 +55,7 @@ import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -92,19 +93,11 @@ internal fun MigrateAnimeDialog(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
flags.forEachIndexed { index, flag ->
val onChange = { selectedFlags[index] = !selectedFlags[index] }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onChange),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = selectedFlags[index],
onCheckedChange = { onChange() },
)
Text(text = context.getString(flag.titleId))
}
LabeledCheckbox(
label = stringResource(flag.titleId),
checked = selectedFlags[index],
onCheckedChange = { selectedFlags[index] = it },
)
}
}
},

View file

@ -34,6 +34,7 @@ import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
@ -55,17 +56,9 @@ class MangaSourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = Injekt.get<MangaSourceManager>().getOrStub(sourceId).toString(),
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
UpIcon()
}
},
AppBar(
title = Injekt.get<MangaSourceManager>().getOrStub(sourceId).toString(),
navigateUp = navigator::pop,
scrollBehavior = it,
)
},

View file

@ -55,6 +55,7 @@ import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -92,19 +93,11 @@ internal fun MigrateMangaDialog(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
flags.forEachIndexed { index, flag ->
val onChange = { selectedFlags[index] = !selectedFlags[index] }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onChange),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = selectedFlags[index],
onCheckedChange = { onChange() },
)
Text(text = context.getString(flag.titleId))
}
LabeledCheckbox(
label = stringResource(flag.titleId),
checked = selectedFlags[index],
onCheckedChange = { selectedFlags[index] = it },
)
}
}
},

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.entries.anime.track
import android.app.Application
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -32,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
@ -75,6 +77,7 @@ import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.model.AnimeTrack
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import uy.kohesive.injekt.Injekt
@ -94,14 +97,14 @@ data class AnimeTrackInfoDialogHomeScreen(
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val sm = rememberScreenModel { Model(animeId, sourceId) }
val screenModel = rememberScreenModel { Model(animeId, sourceId) }
val dateFormat = remember {
UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat().get(),
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
AnimeTrackInfoDialogHome(
trackItems = state.trackItems,
@ -150,7 +153,7 @@ data class AnimeTrackInfoDialogHomeScreen(
},
onNewSearch = {
if (it.tracker is EnhancedAnimeTracker) {
sm.registerEnhancedTracking(it)
screenModel.registerEnhancedTracking(it)
} else {
navigator.push(
TrackServiceSearchScreen(
@ -273,19 +276,19 @@ private data class TrackStatusSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackStatusSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onSelectionChange = screenModel::setSelection,
selections = remember { screenModel.getSelections() },
onConfirm = {
sm.setStatus()
screenModel.setStatus()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -326,20 +329,20 @@ private data class TrackEpisodeSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackItemSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
range = remember { sm.getRange() },
onSelectionChange = screenModel::setSelection,
range = remember { screenModel.getRange() },
onConfirm = {
sm.setEpisode()
screenModel.setEpisode()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -389,20 +392,20 @@ private data class TrackScoreSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackScoreSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onSelectionChange = screenModel::setSelection,
selections = remember { screenModel.getSelections() },
onConfirm = {
sm.setScore()
screenModel.setScore()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -500,7 +503,7 @@ private data class TrackDateSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
@ -519,13 +522,13 @@ private data class TrackDateSelectorScreen(
} else {
stringResource(R.string.track_finished_reading_date)
},
initialSelectedDateMillis = sm.initialSelection,
initialSelectedDateMillis = screenModel.initialSelection,
selectableDates = selectableDates,
onConfirm = {
sm.setDate(it)
screenModel.setDate(it)
navigator.pop()
},
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
onRemove = { screenModel.confirmRemoveDate(navigator) }.takeIf { canRemove },
onDismissRequest = navigator::pop,
)
}
@ -575,7 +578,7 @@ private data class TrackDateRemoverScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
@ -597,7 +600,7 @@ private data class TrackDateRemoverScreen(
)
},
text = {
val serviceName = sm.getName()
val serviceName = screenModel.getName()
Text(
text = if (start) {
stringResource(R.string.track_remove_start_date_conf_text, serviceName)
@ -615,11 +618,11 @@ private data class TrackDateRemoverScreen(
),
) {
TextButton(onClick = navigator::pop) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
FilledTonalButton(
onClick = {
sm.removeDate()
screenModel.removeDate()
navigator.popUntil { it is AnimeTrackInfoDialogHomeScreen }
},
colors = ButtonDefaults.filledTonalButtonColors(
@ -664,7 +667,7 @@ data class TrackServiceSearchScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
animeId = animeId,
currentUrl = currentUrl,
@ -673,18 +676,18 @@ data class TrackServiceSearchScreen(
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
var textFieldValue by remember { mutableStateOf(TextFieldValue(initialQuery)) }
AnimeTrackerSearch(
query = textFieldValue,
onQueryChange = { textFieldValue = it },
onDispatchQuery = { sm.trackingSearch(textFieldValue.text) },
onDispatchQuery = { screenModel.trackingSearch(textFieldValue.text) },
queryResult = state.queryResult,
selected = state.selected,
onSelectedChange = sm::updateSelection,
onSelectedChange = screenModel::updateSelection,
onConfirmSelection = {
sm.registerTracking(state.selected!!)
screenModel.registerTracking(state.selected!!)
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -752,14 +755,14 @@ private data class TrackerAnimeRemoveScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
animeId = animeId,
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val serviceName = sm.getName()
val serviceName = screenModel.getName()
var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
@ -776,23 +779,18 @@ private data class TrackerAnimeRemoveScreen(
)
},
text = {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResource(R.string.track_delete_text, serviceName),
)
if (sm.isDeletable()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = removeRemoteTrack,
onCheckedChange = { removeRemoteTrack = it },
)
Text(
text = stringResource(
R.string.track_delete_remote_text,
serviceName,
),
)
}
if (screenModel.isDeletable()) {
LabeledCheckbox(
label = stringResource(R.string.track_delete_remote_text, serviceName),
checked = removeRemoteTrack,
onCheckedChange = { removeRemoteTrack = it },
)
}
}
},
@ -809,8 +807,8 @@ private data class TrackerAnimeRemoveScreen(
}
FilledTonalButton(
onClick = {
sm.unregisterTracking(serviceId)
if (removeRemoteTrack) sm.deleteAnimeFromService()
screenModel.unregisterTracking(serviceId)
if (removeRemoteTrack) screenModel.deleteAnimeFromService()
navigator.pop()
},
colors = ButtonDefaults.filledTonalButtonColors(

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.entries.manga.track
import android.app.Application
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -32,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
@ -75,6 +77,7 @@ import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.track.manga.interactor.DeleteMangaTrack
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.model.MangaTrack
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import uy.kohesive.injekt.Injekt
@ -94,14 +97,14 @@ data class MangaTrackInfoDialogHomeScreen(
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val sm = rememberScreenModel { Model(mangaId, sourceId) }
val screenModel = rememberScreenModel { Model(mangaId, sourceId) }
val dateFormat = remember {
UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat().get(),
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
MangaTrackInfoDialogHome(
trackItems = state.trackItems,
@ -150,7 +153,7 @@ data class MangaTrackInfoDialogHomeScreen(
},
onNewSearch = {
if (it.tracker is EnhancedMangaTracker) {
sm.registerEnhancedTracking(it)
screenModel.registerEnhancedTracking(it)
} else {
navigator.push(
TrackServiceSearchScreen(
@ -273,19 +276,19 @@ private data class TrackStatusSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackStatusSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onSelectionChange = screenModel::setSelection,
selections = remember { screenModel.getSelections() },
onConfirm = {
sm.setStatus()
screenModel.setStatus()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -326,20 +329,20 @@ private data class TrackChapterSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackItemSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
range = remember { sm.getRange() },
onSelectionChange = screenModel::setSelection,
range = remember { screenModel.getRange() },
onConfirm = {
sm.setChapter()
screenModel.setChapter()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -389,20 +392,20 @@ private data class TrackScoreSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
TrackScoreSelector(
selection = state.selection,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onSelectionChange = screenModel::setSelection,
selections = remember { screenModel.getSelections() },
onConfirm = {
sm.setScore()
screenModel.setScore()
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -500,7 +503,7 @@ private data class TrackDateSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
@ -519,13 +522,13 @@ private data class TrackDateSelectorScreen(
} else {
stringResource(R.string.track_finished_reading_date)
},
initialSelectedDateMillis = sm.initialSelection,
initialSelectedDateMillis = screenModel.initialSelection,
selectableDates = selectableDates,
onConfirm = {
sm.setDate(it)
screenModel.setDate(it)
navigator.pop()
},
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
onRemove = { screenModel.confirmRemoveDate(navigator) }.takeIf { canRemove },
onDismissRequest = navigator::pop,
)
}
@ -575,7 +578,7 @@ private data class TrackDateRemoverScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
@ -597,7 +600,7 @@ private data class TrackDateRemoverScreen(
)
},
text = {
val serviceName = sm.getName()
val serviceName = screenModel.getName()
Text(
text = if (start) {
stringResource(R.string.track_remove_start_date_conf_text, serviceName)
@ -615,11 +618,11 @@ private data class TrackDateRemoverScreen(
),
) {
TextButton(onClick = navigator::pop) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
FilledTonalButton(
onClick = {
sm.removeDate()
screenModel.removeDate()
navigator.popUntil { it is MangaTrackInfoDialogHomeScreen }
},
colors = ButtonDefaults.filledTonalButtonColors(
@ -664,7 +667,7 @@ data class TrackServiceSearchScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
mangaId = mangaId,
currentUrl = currentUrl,
@ -673,18 +676,18 @@ data class TrackServiceSearchScreen(
)
}
val state by sm.state.collectAsState()
val state by screenModel.state.collectAsState()
var textFieldValue by remember { mutableStateOf(TextFieldValue(initialQuery)) }
MangaTrackerSearch(
query = textFieldValue,
onQueryChange = { textFieldValue = it },
onDispatchQuery = { sm.trackingSearch(textFieldValue.text) },
onDispatchQuery = { screenModel.trackingSearch(textFieldValue.text) },
queryResult = state.queryResult,
selected = state.selected,
onSelectedChange = sm::updateSelection,
onSelectedChange = screenModel::updateSelection,
onConfirmSelection = {
sm.registerTracking(state.selected!!)
screenModel.registerTracking(state.selected!!)
navigator.pop()
},
onDismissRequest = navigator::pop,
@ -752,14 +755,14 @@ private data class TrackerMangaRemoveScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
val screenModel = rememberScreenModel {
Model(
mangaId = mangaId,
track = track,
tracker = Injekt.get<TrackerManager>().get(serviceId)!!,
)
}
val serviceName = sm.getName()
val serviceName = screenModel.getName()
var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
@ -776,23 +779,18 @@ private data class TrackerMangaRemoveScreen(
)
},
text = {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResource(R.string.track_delete_text, serviceName),
)
if (sm.isDeletable()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = removeRemoteTrack,
onCheckedChange = { removeRemoteTrack = it },
)
Text(
text = stringResource(
R.string.track_delete_remote_text,
serviceName,
),
)
}
if (screenModel.isDeletable()) {
LabeledCheckbox(
label = stringResource(R.string.track_delete_remote_text, serviceName),
checked = removeRemoteTrack,
onCheckedChange = { removeRemoteTrack = it },
)
}
}
},
@ -809,8 +807,8 @@ private data class TrackerMangaRemoveScreen(
}
FilledTonalButton(
onClick = {
sm.unregisterTracking(serviceId)
if (removeRemoteTrack) sm.deleteMangaFromService()
screenModel.unregisterTracking(serviceId)
if (removeRemoteTrack) screenModel.deleteMangaFromService()
navigator.pop()
},
colors = ButtonDefaults.filledTonalButtonColors(

View file

@ -13,6 +13,7 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.util.Tab
@ -27,6 +28,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
data class HistoriesTab(
private val fromMore: Boolean,
private val preferences: UiPreferences
) : Tab() {
override val options: TabOptions
@ -59,8 +61,8 @@ data class HistoriesTab(
TabbedScreen(
titleRes = R.string.label_recent_manga,
tabs = listOf(
animeHistoryTab(context, fromMore),
mangaHistoryTab(context, fromMore),
animeHistoryTab(context, fromMore, preferences),
mangaHistoryTab(context, fromMore, preferences),
),
mangaSearchQuery = mangaSearchQuery,
onChangeMangaSearchQuery = mangaHistoryScreenModel::search,

View file

@ -13,6 +13,7 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.history.HistoryDeleteAllDialog
@ -34,6 +35,7 @@ val resumeLastEpisodeSeenEvent = Channel<Unit>()
fun Screen.animeHistoryTab(
context: Context,
fromMore: Boolean,
preferences: UiPreferences
): TabContent {
val snackbarHostState = SnackbarHostState()
@ -66,6 +68,7 @@ fun Screen.animeHistoryTab(
onClickCover = { navigator.push(AnimeScreen(it)) },
onClickResume = screenModel::getNextEpisodeForAnime,
onDialogChange = screenModel::setDialog,
preferences = preferences
)
val onDismissRequest = { screenModel.setDialog(null) }

View file

@ -13,8 +13,10 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.components.preferences
import eu.kanade.presentation.history.HistoryDeleteAllDialog
import eu.kanade.presentation.history.HistoryDeleteDialog
import eu.kanade.presentation.history.manga.MangaHistoryScreen
@ -33,6 +35,7 @@ val resumeLastChapterReadEvent = Channel<Unit>()
fun Screen.mangaHistoryTab(
context: Context,
fromMore: Boolean,
preferences: UiPreferences
): TabContent {
val snackbarHostState = SnackbarHostState()
@ -64,6 +67,7 @@ fun Screen.mangaHistoryTab(
onClickCover = { navigator.push(MangaScreen(it)) },
onClickResume = screenModel::getNextChapterForManga,
onDialogChange = screenModel::setDialog,
preferences = preferences
)
val onDismissRequest = { screenModel.setDialog(null) }

View file

@ -35,6 +35,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabNavigator
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.util.Screen
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.R
@ -72,6 +73,8 @@ object HomeScreen : Screen() {
private val libraryPreferences: LibraryPreferences by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
val tabsNoHistory = listOf(
AnimeLibraryTab,
MangaLibraryTab,
@ -83,7 +86,7 @@ object HomeScreen : Screen() {
val tabsNoUpdates = listOf(
AnimeLibraryTab,
MangaLibraryTab,
HistoriesTab(false),
HistoriesTab(false, uiPreferences),
BrowseTab(),
MoreTab,
)
@ -91,7 +94,7 @@ object HomeScreen : Screen() {
val tabsNoManga = listOf(
AnimeLibraryTab,
UpdatesTab(fromMore = false, inMiddle = false),
HistoriesTab(false),
HistoriesTab(false, uiPreferences),
BrowseTab(),
MoreTab,
)
@ -193,7 +196,7 @@ object HomeScreen : Screen() {
libraryPreferences.bottomNavStyle().get() == 1,
libraryPreferences.bottomNavStyle().get() == 0,
)
is Tab.History -> HistoriesTab(false)
is Tab.History -> HistoriesTab(false, uiPreferences)
is Tab.Browse -> BrowseTab(it.toExtensions)
is Tab.More -> MoreTab
}

View file

@ -104,6 +104,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
@ -307,8 +308,10 @@ class MainActivity : BaseActivity() {
setSplashScreenExitAnimation(splashScreen)
if (isLaunch && libraryPreferences.autoClearItemCache().get()) {
chapterCache.clear()
episodeCache.clear()
lifecycleScope.launchIO {
chapterCache.clear()
episodeCache.clear()
}
}
externalPlayerResult = registerForActivityResult(
@ -500,11 +503,6 @@ class MainActivity : BaseActivity() {
}
companion object {
// Splash screen
private const val SPLASH_MIN_DURATION = 500 // ms
private const val SPLASH_MAX_DURATION = 5000 // ms
private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
const val INTENT_ANIMESEARCH = "eu.kanade.tachiyomi.ANIMESEARCH"
const val INTENT_SEARCH_QUERY = "query"
@ -533,4 +531,10 @@ class MainActivity : BaseActivity() {
}
}
}
}
// Splash screen
private const val SPLASH_MIN_DURATION = 500 // ms
private const val SPLASH_MAX_DURATION = 5000 // ms
private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms

View file

@ -19,6 +19,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.more.MoreScreen
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
@ -89,8 +90,10 @@ object MoreTab : Tab() {
private val libraryPreferences: LibraryPreferences by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val altOpen = when (libraryPreferences.bottomNavStyle().get()) {
0 -> HistoriesTab(true)
0 -> HistoriesTab(true, uiPreferences)
1 -> UpdatesTab(fromMore = true, inMiddle = false)
else -> MangaLibraryTab
}

View file

@ -512,7 +512,7 @@ class ExternalIntents {
tracker.animeService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
} else {
delayedTrackingStore.addAnimeItem(updatedTrack)
delayedTrackingStore.addAnime(track.animeId, lastEpisodeSeen = episodeNumber)
DelayedAnimeTrackingUpdateJob.setupTask(context)
}
}

View file

@ -13,19 +13,13 @@ import android.graphics.Paint
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View.LAYER_TYPE_HARDWARE
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
@ -44,17 +38,15 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.internal.ToolbarUtils
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.reader.BottomReaderBar
import eu.kanade.presentation.reader.ChapterNavigator
import eu.kanade.presentation.reader.OrientationModeSelectDialog
import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.ReaderPageActionsDialog
import eu.kanade.presentation.reader.ReadingModeSelectDialog
import eu.kanade.presentation.reader.appbars.ReaderAppBars
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
@ -75,15 +67,12 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
@ -255,7 +244,7 @@ class ReaderActivity : BaseActivity() {
override fun onResume() {
super.onResume()
viewModel.restartReadTimer()
setMenuVisibility(viewModel.state.value.menuVisible, animate = false)
setMenuVisibility(viewModel.state.value.menuVisible)
}
/**
@ -265,7 +254,7 @@ class ReaderActivity : BaseActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
setMenuVisibility(viewModel.state.value.menuVisible, animate = false)
setMenuVisibility(viewModel.state.value.menuVisible)
}
}
@ -274,50 +263,6 @@ class ReaderActivity : BaseActivity() {
assistUrl?.let { outContent.webUri = it.toUri() }
}
/**
* Called when the options menu of the toolbar is being created. It adds our custom menu.
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.reader, menu)
val isChapterBookmarked = viewModel.getCurrentChapter()?.chapter?.bookmark ?: false
menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked
menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked
val isHttpSource = viewModel.getSource() is HttpSource
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
menu.findItem(R.id.action_share).isVisible = isHttpSource
return true
}
/**
* Called when an item of the options menu was clicked. Used to handle clicks on our menu
* entries.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_open_in_web_view -> {
openChapterInWebView()
}
R.id.action_bookmark -> {
viewModel.bookmarkCurrentChapter(true)
invalidateOptionsMenu()
}
R.id.action_remove_bookmark -> {
viewModel.bookmarkCurrentChapter(false)
invalidateOptionsMenu()
}
R.id.action_share -> {
assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain")
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
}
}
return super.onOptionsItemSelected(item)
}
/**
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
@ -360,32 +305,9 @@ class ReaderActivity : BaseActivity() {
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.toolbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
margin(top = true, horizontal = true)
}
}
binding.readerMenuBottom.applyInsetter {
binding.dialogRoot.applyInsetter {
type(navigationBars = true) {
margin(bottom = true, horizontal = true)
}
}
binding.toolbar.setOnClickListener {
viewModel.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = Constants.SHORTCUT_MANGA
putExtra(Constants.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
},
)
margin(vertical = true, horizontal = true)
}
}
@ -401,7 +323,7 @@ class ReaderActivity : BaseActivity() {
}
}
binding.readerMenuBottom.setComposeContent {
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
val settingsScreenModel = remember {
ReaderSettingsScreenModel(
@ -412,6 +334,57 @@ class ReaderActivity : BaseActivity() {
)
}
val isHttpSource = viewModel.getSource() is HttpSource
val isFullscreen by readerPreferences.fullscreen().collectAsState()
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
ReaderAppBars(
visible = state.menuVisible,
fullscreen = isFullscreen,
mangaTitle = state.manga?.title,
chapterTitle = state.currentChapter?.chapter?.name,
navigateUp = onBackPressedDispatcher::onBackPressed,
onClickTopAppBar = ::openMangaScreen,
bookmarked = state.bookmarked,
onToggleBookmarked = viewModel::toggleChapterBookmark,
onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource },
onShare = ::shareChapter.takeIf { isHttpSource },
viewer = state.viewer,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onSliderValueChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
readingMode = ReadingModeType.fromPreference(
viewModel.getMangaReadingMode(resolveDefault = false),
),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientationMode = OrientationType.fromPreference(
viewModel.getMangaOrientationType(resolveDefault = false),
),
onClickOrientationMode = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(if (enabled) R.string.on else R.string.off)
},
onClickSettings = viewModel::openSettingsDialog,
)
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
@ -471,68 +444,9 @@ class ReaderActivity : BaseActivity() {
}
}
binding.readerMenuBottom.setComposeContent {
val state by viewModel.state.collectAsState()
if (state.viewer == null) return@setComposeContent
val isRtl = state.viewer is R2LPagerViewer
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onSliderValueChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
)
BottomReaderBar(
readingMode = ReadingModeType.fromPreference(
viewModel.getMangaReadingMode(resolveDefault = false),
),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientationMode = OrientationType.fromPreference(
viewModel.getMangaOrientationType(resolveDefault = false),
),
onClickOrientationMode = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
},
onClickSettings = viewModel::openSettingsDialog,
)
}
}
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
}
val toolbarColor = ColorUtils.setAlphaComponent(
toolbarBackground.resolvedTintColor,
toolbarBackground.alpha,
SurfaceColors.SURFACE_2.getColor(this),
if (isNightMode()) 230 else 242, // 90% dark 95% light
)
window.statusBarColor = toolbarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@ -544,56 +458,18 @@ class ReaderActivity : BaseActivity() {
}
/**
* Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views.
* Sets the visibility of the menu according to [visible].
*/
private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
private fun setMenuVisibility(visible: Boolean) {
viewModel.showMenus(visible)
if (visible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
binding.readerMenu.isVisible = true
if (animate) {
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
toolbarAnimation.applySystemAnimatorScale(this)
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationStart(animation: Animation) {
// Fix status bar being translucent the first time it's opened.
window.addFlags(
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
)
}
},
)
binding.toolbar.startAnimation(toolbarAnimation)
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
bottomAnimation.applySystemAnimatorScale(this)
binding.readerMenuBottom.startAnimation(bottomAnimation)
}
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
} else {
if (readerPreferences.fullscreen().get()) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
if (animate) {
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
toolbarAnimation.applySystemAnimatorScale(this)
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationEnd(animation: Animation) {
binding.readerMenu.isVisible = false
}
},
)
binding.toolbar.startAnimation(toolbarAnimation)
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
bottomAnimation.applySystemAnimatorScale(this)
binding.readerMenuBottom.startAnimation(bottomAnimation)
}
}
}
@ -627,14 +503,24 @@ class ReaderActivity : BaseActivity() {
showReadingModeToast(viewModel.getMangaReadingMode())
}
supportActionBar?.title = manga.title
loadingIndicator = ReaderProgressIndicator(this)
binding.readerContainer.addView(loadingIndicator)
startPostponedEnterTransition()
}
private fun openMangaScreen() {
viewModel.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = Constants.SHORTCUT_MANGA
putExtra(Constants.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
},
)
}
}
private fun openChapterInWebView() {
val manga = viewModel.manga ?: return
val source = viewModel.getSource() ?: return
@ -644,6 +530,13 @@ class ReaderActivity : BaseActivity() {
}
}
private fun shareChapter() {
assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain")
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
}
private fun showReadingModeToast(mode: Int) {
try {
readingModeToast?.cancel()
@ -663,15 +556,6 @@ class ReaderActivity : BaseActivity() {
binding.readerContainer.removeView(loadingIndicator)
viewModel.state.value.viewer?.setChapters(viewerChapters)
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
ToolbarUtils.getSubtitleTextView(binding.toolbar)?.let {
it.ellipsize = TextUtils.TruncateAt.MARQUEE
it.isSelected = true
}
// Invalidate menu to show proper chapter bookmark state
invalidateOptionsMenu()
lifecycleScope.launchIO {
viewModel.getChapterUrl()?.let { url ->
assistUrl = url
@ -709,7 +593,7 @@ class ReaderActivity : BaseActivity() {
*/
private fun moveToPageIndex(index: Int) {
val viewer = viewModel.state.value.viewer ?: return
val currentChapter = viewModel.getCurrentChapter() ?: return
val currentChapter = viewModel.state.value.currentChapter ?: return
val page = currentChapter.pages?.getOrNull(index) ?: return
viewer.moveToPage(page)
}
@ -905,11 +789,9 @@ class ReaderActivity : BaseActivity() {
.onEach(::setTrueColor)
.launchIn(lifecycleScope)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
readerPreferences.cutoutShort().changes()
.onEach(::setCutoutShort)
.launchIn(lifecycleScope)
}
readerPreferences.cutoutShort().changes()
.onEach(::setCutoutShort)
.launchIn(lifecycleScope)
readerPreferences.keepScreenOn().changes()
.onEach(::setKeepScreenOn)
@ -969,8 +851,9 @@ class ReaderActivity : BaseActivity() {
}
}
@TargetApi(Build.VERSION_CODES.P)
private fun setCutoutShort(enabled: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
window.attributes.layoutInDisplayCutoutMode = when (enabled) {
true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER

View file

@ -318,7 +318,10 @@ class ReaderViewModel @JvmOverloads constructor(
it.viewerChapters?.unref()
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
it.copy(viewerChapters = newChapters)
it.copy(
viewerChapters = newChapters,
bookmarked = newChapters.currChapter.chapter.bookmark,
)
}
}
return newChapters
@ -587,8 +590,8 @@ class ReaderViewModel @JvmOverloads constructor(
/**
* Returns the currently active chapter.
*/
fun getCurrentChapter(): ReaderChapter? {
return state.value.viewerChapters?.currChapter
private fun getCurrentChapter(): ReaderChapter? {
return state.value.currentChapter
}
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
@ -608,9 +611,11 @@ class ReaderViewModel @JvmOverloads constructor(
/**
* Bookmarks the currently active chapter.
*/
fun bookmarkCurrentChapter(bookmarked: Boolean) {
fun toggleChapterBookmark() {
val chapter = getCurrentChapter()?.chapter ?: return
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
val bookmarked = !chapter.bookmark
chapter.bookmark = bookmarked
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
@ -619,6 +624,12 @@ class ReaderViewModel @JvmOverloads constructor(
),
)
}
mutableState.update {
it.copy(
bookmarked = bookmarked,
)
}
}
/**
@ -901,6 +912,7 @@ class ReaderViewModel @JvmOverloads constructor(
data class State(
val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null,
val bookmarked: Boolean = false,
val isLoadingAdjacentChapter: Boolean = false,
val currentPage: Int = -1,
@ -911,8 +923,11 @@ class ReaderViewModel @JvmOverloads constructor(
val dialog: Dialog? = null,
val menuVisible: Boolean = false,
) {
val currentChapter: ReaderChapter?
get() = viewerChapters?.currChapter
val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1
get() = currentChapter?.pages?.size ?: -1
}
sealed interface Dialog {

View file

@ -41,13 +41,13 @@ open class GestureDetectorWithLongTap(
// This is the key difference with the built-in detector. We have to ignore the
// event if the last up and current down are too close in time (double tap).
if (ev.downTime - lastUp > doubleTapTime) {
downX = ev.rawX
downY = ev.rawY
downX = ev.x
downY = ev.y
handler.postDelayed(longTapFn, longTapTime)
}
}
MotionEvent.ACTION_MOVE -> {
if (abs(ev.rawX - downX) > slop || abs(ev.rawY - downY) > slop) {
if (abs(ev.x - downX) > slop || abs(ev.y - downY) > slop) {
handler.removeCallbacks(longTapFn)
}
}

View file

@ -102,7 +102,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
},
)
pager.tapListener = { event ->
val pos = PointF(event.rawX / pager.width, event.rawY / pager.height)
val pos = PointF(event.x / pager.width, event.y / pager.height)
when (config.navigator.getAction(pos)) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT -> moveToNext()

View file

@ -111,7 +111,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
},
)
recycler.tapListener = { event ->
val pos = PointF(event.rawX / recycler.width, event.rawY / recycler.height)
val pos = PointF(event.y / recycler.width, event.y / recycler.height)
when (config.navigator.getAction(pos)) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()

View file

@ -36,7 +36,7 @@ class CrashLogUtil(private val context: Context) {
Device name: ${Build.DEVICE}
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
WebView user agent: ${WebViewUtil.getInferredUserAgent(context)}
WebView: ${WebViewUtil.getVersion(context)}
""".trimIndent()
}
}

View file

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.widget.listener
import android.view.animation.Animation
open class SimpleAnimationListener : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {}
override fun onAnimationStart(animation: Animation) {}
}

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="100%"
android:toYDelta="0%" />
</set>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="-100%"
android:toYDelta="0%" />
</set>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="0%"
android:toYDelta="100%" />
</set>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="0%"
android:toYDelta="-100%" />
</set>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

View file

@ -1,5 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
@ -43,28 +42,6 @@
android:layout_height="match_parent"
android:visibility="gone" />
<FrameLayout
android:id="@+id/reader_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="?attr/actionBarTheme"
android:visibility="invisible"
tools:visibility="visible">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_menu_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
</FrameLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_bookmark"
android:title="@string/action_bookmark"
app:showAsAction="never" />
<item
android:id="@+id/action_share"
android:title="@string/action_share"
app:showAsAction="never" />
<item
android:id="@+id/action_remove_bookmark"
android:icon="@drawable/ic_bookmark_24dp"
android:title="@string/action_remove_bookmark"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_open_in_web_view"
android:icon="@drawable/ic_webview_24dp"
android:title="@string/action_open_in_web_view"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -8,6 +8,28 @@ import nl.adaptivity.xmlutil.serialization.XmlValue
const val COMIC_INFO_FILE = "ComicInfo.xml"
fun SManga.getComicInfo() = ComicInfo(
series = ComicInfo.Series(title),
summary = description?.let { ComicInfo.Summary(it) },
writer = author?.let { ComicInfo.Writer(it) },
penciller = artist?.let { ComicInfo.Penciller(it) },
genre = genre?.let { ComicInfo.Genre(it) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(status.toLong()),
),
title = null,
number = null,
web = null,
translator = null,
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
categories = null,
)
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.series?.let { title = it.value }
comicInfo.writer?.let { author = it.value }
@ -39,6 +61,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
}
// https://anansi-project.github.io/docs/comicinfo/schemas/v2.0
@Suppress("UNUSED")
@Serializable
@XmlSerialName("ComicInfo", "", "")
data class ComicInfo(
@ -59,12 +83,10 @@ data class ComicInfo(
val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?,
) {
@Suppress("UNUSED")
@XmlElement(false)
@XmlSerialName("xmlns:xsd", "", "")
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
@Suppress("UNUSED")
@XmlElement(false)
@XmlSerialName("xmlns:xsi", "", "")
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"

View file

@ -63,6 +63,10 @@ class AndroidPreferenceStore(
deserializer = deserializer,
)
}
override fun getAll(): Map<String, *> {
return sharedPreferences.all ?: emptyMap<String, Any>()
}
}
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -22,3 +22,41 @@ fun File.getUriCompat(context: Context): Uri {
this.toUri()
}
}
/**
* Copies this file to the given [target] file while marking the file as read-only.
*
* @see File.copyTo
*/
fun File.copyAndSetReadOnlyTo(target: File, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): File {
if (!this.exists()) {
throw NoSuchFileException(file = this, reason = "The source file doesn't exist.")
}
if (target.exists()) {
if (!overwrite) {
throw FileAlreadyExistsException(file = this, other = target, reason = "The destination file already exists.")
} else if (!target.delete()) {
throw FileAlreadyExistsException(file = this, other = target, reason = "Tried to overwrite the destination, but failed to delete it.")
}
}
if (this.isDirectory) {
if (!target.mkdirs()) {
throw FileSystemException(file = this, other = target, reason = "Failed to create target directory.")
}
} else {
target.parentFile?.mkdirs()
this.inputStream().use { input ->
target.outputStream().use { output ->
// Set read-only
target.setReadOnly()
input.copyTo(output, bufferSize)
}
}
}
return target
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
@ -33,6 +34,18 @@ object WebViewUtil {
.replace("Version/.* Chrome/".toRegex(), "Chrome/")
}
fun getVersion(context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?"
val pm = context.packageManager
val label = webView.applicationInfo.loadLabel(pm)
val version = webView.versionName
"$label $version"
} else {
"Unknown"
}
}
fun supportsWebView(context: Context): Boolean {
try {
// May throw android.webkit.WebViewFactory$MissingWebViewPackageException if WebView

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
}
}
}

View file

@ -20,6 +20,8 @@ interface PreferenceStore {
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T>
fun getAll(): Map<String, *>
}
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(

View file

@ -63,8 +63,6 @@ class LibraryPreferences(
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
fun showContinueViewingButton() =
preferenceStore.getBoolean("display_continue_reading_button", false)

View file

@ -12,7 +12,7 @@ biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
corektx = "androidx.core:core-ktx:1.12.0"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.2"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
glance = "androidx.glance:glance-appwidget:1.0.0"
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1"
@ -22,17 +22,15 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
guava = "com.google.guava:guava:32.1.2-android"
workmanager = "androidx.work:work-runtime-ktx:2.8.1"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-rc02"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
[bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
workmanager = ["work-runtime", "guava"]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]

View file

@ -2,7 +2,7 @@
aboutlib_version = "10.9.1"
okhttp_version = "5.0.0-alpha.11"
shizuku_version = "12.2.0"
sqlite = "2.3.1"
sqlite = "2.4.0"
sqldelight = "2.0.0"
leakcanary = "2.12"
voyager = "1.0.0-rc07"
@ -88,7 +88,7 @@ voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.6.0"
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.6.1"
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"

View file

@ -165,7 +165,6 @@
<string name="pref_clear_manga_database_summary">Delete history for manga that are not saved in your library</string>
<string name="pref_clear_anime_database_summary">Delete history for anime that are not saved in your library</string>
<string name="clear_database_confirmation">Are you sure? Completed episodes and chapters and progress of non-library entries will be lost</string>
<string name="pref_refresh_library_tracking_summary">Updates status, score and progress from the tracking services</string>
<string name="pref_incognito_mode_summary">Pauses your history</string>
<string name="manga_from_library">Manga from library</string>
<string name="anime_from_library">Anime from library</string>

View file

@ -268,8 +268,6 @@
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
<string name="pref_library_update_refresh_trackers_summary">Update trackers when updating library</string>
<string name="default_category">Default category</string>
<string name="default_category_summary">Always ask</string>
@ -528,7 +526,6 @@
<string name="pref_clear_webview_data">Clear WebView data</string>
<string name="webview_data_deleted">WebView data cleared</string>
<string name="pref_refresh_library_covers">Refresh library covers</string>
<string name="pref_refresh_library_tracking">Refresh tracking</string>
<string name="pref_reset_viewer_flags">Reset per-series reader settings</string>
<string name="pref_reset_viewer_flags_summary">Resets reading mode and orientation of all series</string>
<string name="pref_reset_viewer_flags_success">All reader settings reset</string>
@ -852,6 +849,7 @@
<string name="file_select_cover">Select cover image</string>
<string name="file_select_backup">Select backup file</string>
<string name="file_picker_error">No file picker app found</string>
<string name="file_null_uri_error">File picker failed to return file to app</string>
<!--UpdateCheck-->
<string name="update_check_confirm">Download</string>

View file

@ -175,7 +175,10 @@ fun AdaptiveSheet(
.offset {
IntOffset(
0,
anchoredDraggableState.offset.takeIf { it.isFinite() }?.roundToInt() ?: 0,
anchoredDraggableState.offset
.takeIf { it.isFinite() }
?.roundToInt()
?: 0,
)
}
.anchoredDraggable(
@ -245,8 +248,13 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
settle(velocity = available.toFloat())
return available
val toFling = available.toFloat()
return if (toFling > 0) {
settle(toFling)
available
} else {
Velocity.Zero
}
}
private fun Float.toOffset(): Offset = Offset(0f, this)

View file

@ -0,0 +1,44 @@
package tachiyomi.presentation.core.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
@Composable
fun LabeledCheckbox(
modifier: Modifier = Modifier,
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = modifier
.clip(MaterialTheme.shapes.small)
.fillMaxWidth()
.heightIn(min = 48.dp)
.clickable(
role = Role.Checkbox,
onClick = { onCheckedChange(!checked) },
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Checkbox(
checked = checked,
onCheckedChange = null,
)
Text(text = label)
}
}

View file

@ -49,7 +49,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@ -195,6 +197,8 @@ fun SliderItem(
valueText: String,
onChange: (Int) -> Unit,
) {
val haptic = LocalHapticFeedback.current
Row(
modifier = Modifier
.fillMaxWidth()
@ -215,7 +219,13 @@ fun SliderItem(
Slider(
value = value.toFloat(),
onValueChange = { onChange(it.toInt()) },
onValueChange = {
val newValue = it.toInt()
if (newValue != value) {
onChange(newValue)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
},
modifier = Modifier.weight(1.5f),
valueRange = min.toFloat()..max.toFloat(),
steps = max - min,

View file

@ -1,6 +1,7 @@
package tachiyomi.source.local.entries.manga
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.UnmeteredSource
@ -19,6 +20,7 @@ import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.ChapterDetails
import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.util.lang.withIOContext
@ -129,22 +131,20 @@ actual class LocalMangaSource(
// Fetch chapters of all the manga
mangas.forEach { manga ->
runBlocking {
val chapters = getChapterList(manga)
if (chapters.isNotEmpty()) {
val chapter = chapters.last()
val format = getFormat(chapter)
val chapters = getChapterList(manga)
if (chapters.isNotEmpty()) {
val chapter = chapters.last()
val format = getFormat(chapter)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(manga)
}
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(manga)
}
}
// Copy the cover from the first chapter found if not available
if (manga.thumbnail_url == null) {
updateCover(chapter, manga)
}
// Copy the cover from the first chapter found if not available
if (manga.thumbnail_url == null) {
updateCover(chapter, manga)
}
}
}
@ -160,6 +160,7 @@ actual class LocalMangaSource(
// Augment manga details based on metadata files
try {
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile = mangaDirFiles
@ -176,7 +177,8 @@ actual class LocalMangaSource(
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
// Old custom JSON format
// TODO: remove support for this entirely after a while
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
@ -186,6 +188,16 @@ actual class LocalMangaSource(
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
// Replace with ComicInfo.xml file
val comicInfo = manga.getComicInfo()
UniFile.fromFile(mangaDir)
?.createFile(COMIC_INFO_FILE)
?.openOutputStream()
?.use {
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
it.write(comicInfoString.toByteArray())
legacyJsonDetailsFile.delete()
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
@ -194,7 +206,6 @@ actual class LocalMangaSource(
.filter(ArchiveManga::isSupported)
.toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)

View file

@ -18,7 +18,7 @@ actual class LocalMangaCoverManager(
actual fun find(mangaUrl: String): File? {
return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with 'cover'
// Get all file whose names start with "cover"
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
.firstOrNull {