mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
76df725cab
commit
150d43e325
98 changed files with 1613 additions and 1142 deletions
5
.github/renovate.json5
vendored
5
.github/renovate.json5
vendored
|
@ -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",
|
||||
|
|
|
@ -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
3
app/.gitignore
vendored
|
@ -1,4 +1,3 @@
|
|||
/build
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
*.iml
|
|
@ -200,7 +200,7 @@ dependencies {
|
|||
implementation(androidx.bundles.lifecycle)
|
||||
|
||||
// Job scheduling
|
||||
implementation(androidx.bundles.workmanager)
|
||||
implementation(androidx.workmanager)
|
||||
|
||||
// RxJava
|
||||
implementation(libs.rxjava)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) }
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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())))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()) }
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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())))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ private fun ItemDeleteDialog(
|
|||
onDismissRequest()
|
||||
},
|
||||
content = {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -63,6 +63,10 @@ class AndroidPreferenceStore(
|
|||
deserializer = deserializer,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAll(): Map<String, *> {
|
||||
return sharedPreferences.all ?: emptyMap<String, Any>()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue