mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
5b12c144da | |||
f38130d086 | |||
4b60138d41 | |||
fde7bfa3d1 | |||
69635ee66a | |||
224f29077d | |||
e1ab1fdb65 | |||
3e86cb094b | |||
9fbd3fe33f | |||
073e9f94ff | |||
64c0d9506d | |||
f638092ab9 | |||
d0c4463ab3 | |||
ad107860b9 | |||
5efb31bd71 | |||
e4a2f35907 | |||
e49781de7a | |||
37cb4ec0c2 | |||
401134fa8e | |||
87391832ba | |||
e36d31bf0f | |||
37b7efbc87 | |||
6e4a30e593 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.14.0)
|
||||
- To the latest version of the app (stable is v0.14.1)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.14.0"
|
||||
Example: "0.14.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.14.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.14.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
@ -27,8 +27,8 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 89
|
||||
versionName = "0.14.0"
|
||||
versionCode = 90
|
||||
versionName = "0.14.1"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -329,6 +329,19 @@ tasks {
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
|
||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
project.buildDir.absolutePath + "/compose_metrics"
|
||||
)
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
project.buildDir.absolutePath + "/compose_metrics"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
preBuild {
|
||||
|
@ -63,6 +63,7 @@ import eu.kanade.domain.source.repository.SourceDataRepository
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.track.interactor.DeleteTrack
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
@ -104,6 +105,7 @@ class DomainModule : InjektModule {
|
||||
|
||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||
addFactory { DeleteTrack(get()) }
|
||||
addFactory { GetTracksPerManga(get()) }
|
||||
addFactory { GetTracks(get()) }
|
||||
addFactory { InsertTrack(get()) }
|
||||
|
||||
|
@ -27,7 +27,12 @@ class SetReadStatus(
|
||||
}
|
||||
|
||||
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
|
||||
val chaptersToUpdate = chapters.filterNot { it.read == read }
|
||||
val chaptersToUpdate = chapters.filter {
|
||||
when (read) {
|
||||
true -> !it.read
|
||||
false -> it.read || it.lastPageRead > 0
|
||||
}
|
||||
}
|
||||
if (chaptersToUpdate.isEmpty()) {
|
||||
return@withNonCancellableContext Result.NoChapters
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ class GetNextChapter(
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
|
||||
val chapter = getChapter.await(chapterId)!!
|
||||
val manga = getManga.await(mangaId)!!
|
||||
val chapter = getChapter.await(chapterId) ?: return null
|
||||
val manga = getManga.await(mangaId) ?: return null
|
||||
|
||||
if (!chapter.read) return chapter
|
||||
|
||||
|
@ -19,10 +19,6 @@ class GetTracks(
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe(): Flow<List<Track>> {
|
||||
return trackRepository.getTracksAsFlow()
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<List<Track>> {
|
||||
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetTracksPerManga(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<Long, List<Long>>> {
|
||||
return trackRepository.getTracksAsFlow().map { tracks ->
|
||||
tracks
|
||||
.groupBy { it.mangaId }
|
||||
.mapValues { entry ->
|
||||
entry.value.map { it.syncId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,12 +13,12 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
@ -89,7 +89,7 @@ fun BrowseSourceScreen(
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
BrowseSourceToolbar(
|
||||
state = presenter,
|
||||
source = presenter.source!!,
|
||||
source = presenter.source,
|
||||
displayMode = presenter.displayMode,
|
||||
onDisplayModeChange = { presenter.displayMode = it },
|
||||
navigateUp = navigateUp,
|
||||
@ -253,7 +253,7 @@ fun BrowseSourceContent(
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.local_source_help_guide,
|
||||
icon = Icons.Default.HelpOutline,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onLocalSourceHelpClick,
|
||||
),
|
||||
)
|
||||
@ -261,17 +261,17 @@ fun BrowseSourceContent(
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_retry,
|
||||
icon = Icons.Default.Refresh,
|
||||
icon = Icons.Outlined.Refresh,
|
||||
onClick = mangaList::refresh,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_open_in_web_view,
|
||||
icon = Icons.Default.Public,
|
||||
icon = Icons.Outlined.Public,
|
||||
onClick = onWebViewClick,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.label_help,
|
||||
icon = Icons.Default.HelpOutline,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onHelpClick,
|
||||
),
|
||||
)
|
||||
|
@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@ -368,7 +368,7 @@ private fun ExtensionItemActions(
|
||||
} else {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel),
|
||||
)
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Dangerous
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
@ -48,7 +47,7 @@ fun SourceIcon(
|
||||
when {
|
||||
source.isStub && icon == null -> {
|
||||
Image(
|
||||
imageVector = Icons.Default.Warning,
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
||||
modifier = modifier.then(defaultModifier),
|
||||
@ -85,7 +84,7 @@ fun ExtensionIcon(
|
||||
placeholder = ColorPainter(Color(0x1F888888)),
|
||||
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
.clip(MaterialTheme.shapes.extraSmall),
|
||||
)
|
||||
}
|
||||
is Extension.Installed -> {
|
||||
@ -105,7 +104,7 @@ fun ExtensionIcon(
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> Image(
|
||||
imageVector = Icons.Default.Dangerous,
|
||||
imageVector = Icons.Filled.Dangerous,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
||||
modifier = modifier.then(defaultModifier),
|
||||
|
@ -1,28 +1,22 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.library.components.MangaGridComfortableText
|
||||
import eu.kanade.presentation.library.components.MangaGridCover
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@ -37,9 +31,9 @@ fun BrowseSourceComfortableGrid(
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = columns,
|
||||
contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = contentPadding + PaddingValues(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
||||
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
||||
) {
|
||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
@ -71,36 +65,22 @@ fun BrowseSourceComfortableGridItem(
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = onClick,
|
||||
) {
|
||||
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
),
|
||||
) {
|
||||
MangaGridCover(
|
||||
cover = {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (manga.favorite) {
|
||||
drawRect(overlayColor)
|
||||
}
|
||||
},
|
||||
data = manga.thumbnailUrl,
|
||||
)
|
||||
},
|
||||
badgesStart = {
|
||||
if (manga.favorite) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
},
|
||||
)
|
||||
MangaGridComfortableText(
|
||||
text = manga.title,
|
||||
)
|
||||
}
|
||||
MangaComfortableGridItem(
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||
coverBadgeStart = {
|
||||
if (manga.favorite) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
},
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
@ -1,35 +1,22 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.library.components.MangaGridCompactText
|
||||
import eu.kanade.presentation.library.components.MangaGridCover
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@ -44,12 +31,12 @@ fun BrowseSourceCompactGrid(
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = columns,
|
||||
contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = contentPadding + PaddingValues(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
||||
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
BrowseSourceLoadingItem()
|
||||
}
|
||||
}
|
||||
@ -64,8 +51,8 @@ fun BrowseSourceCompactGrid(
|
||||
)
|
||||
}
|
||||
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
|
||||
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
BrowseSourceLoadingItem()
|
||||
}
|
||||
}
|
||||
@ -73,57 +60,27 @@ fun BrowseSourceCompactGrid(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceCompactGridItem(
|
||||
private fun BrowseSourceCompactGridItem(
|
||||
manga: Manga,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = onClick,
|
||||
) {
|
||||
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
|
||||
MangaGridCover(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
),
|
||||
cover = {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (manga.favorite) {
|
||||
drawRect(overlayColor)
|
||||
}
|
||||
},
|
||||
data = eu.kanade.domain.manga.model.MangaCover(
|
||||
manga.id,
|
||||
manga.source,
|
||||
manga.favorite,
|
||||
manga.thumbnailUrl,
|
||||
manga.coverLastModified,
|
||||
),
|
||||
)
|
||||
},
|
||||
badgesStart = {
|
||||
MangaCompactGridItem(
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||
coverBadgeStart = {
|
||||
if (manga.favorite) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color(0xAA000000),
|
||||
),
|
||||
)
|
||||
.fillMaxHeight(0.33f)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
MangaGridCompactText(manga.title)
|
||||
},
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ fun RemoveMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -1,26 +1,21 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.library.components.MangaListItem
|
||||
import eu.kanade.presentation.library.components.MangaListItemContent
|
||||
import eu.kanade.presentation.components.MangaListItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.verticalPadding
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
@ -32,7 +27,7 @@ fun BrowseSourceList(
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
item {
|
||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
||||
@ -64,31 +59,22 @@ fun BrowseSourceListItem(
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = onClick,
|
||||
) {
|
||||
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
|
||||
MangaListItem(
|
||||
coverContent = {
|
||||
MangaCover.Square(
|
||||
modifier = Modifier
|
||||
.padding(vertical = verticalPadding)
|
||||
.fillMaxHeight()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (manga.favorite) {
|
||||
drawRect(overlayColor)
|
||||
}
|
||||
},
|
||||
data = manga.thumbnailUrl,
|
||||
)
|
||||
},
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
badges = {
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||
badge = {
|
||||
if (manga.favorite) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
},
|
||||
content = {
|
||||
MangaListItemContent(text = manga.title)
|
||||
},
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
||||
@Composable
|
||||
fun BrowseSourceToolbar(
|
||||
state: BrowseSourceState,
|
||||
source: CatalogueSource,
|
||||
source: CatalogueSource?,
|
||||
displayMode: LibraryDisplayMode,
|
||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
@ -44,7 +44,7 @@ fun BrowseSourceToolbar(
|
||||
) {
|
||||
if (state.searchQuery == null) {
|
||||
BrowseSourceRegularToolbar(
|
||||
title = if (state.isUserQuery) state.currentFilter.query else source.name,
|
||||
title = if (state.isUserQuery) state.currentFilter.query else source?.name.orEmpty(),
|
||||
isLocalSource = source is LocalSource,
|
||||
displayMode = displayMode,
|
||||
onDisplayModeChange = onDisplayModeChange,
|
||||
|
@ -130,7 +130,7 @@ fun CategoryDeleteDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
|
@ -18,8 +18,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun CategoryListItem(
|
||||
@ -64,10 +66,10 @@ fun CategoryListItem(
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(imageVector = Icons.Outlined.Edit, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,9 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -55,7 +53,7 @@ fun AppBar(
|
||||
subtitle: String? = null,
|
||||
// Up button
|
||||
navigateUp: (() -> Unit)? = null,
|
||||
navigationIcon: ImageVector = Icons.Default.ArrowBack,
|
||||
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
|
||||
// Menu
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
// Action mode
|
||||
@ -105,7 +103,7 @@ fun AppBar(
|
||||
titleContent: @Composable () -> Unit,
|
||||
// Up button
|
||||
navigateUp: (() -> Unit)? = null,
|
||||
navigationIcon: ImageVector = Icons.Default.ArrowBack,
|
||||
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
|
||||
// Menu
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
// Action mode
|
||||
@ -125,7 +123,7 @@ fun AppBar(
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onCancelActionMode) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel),
|
||||
)
|
||||
}
|
||||
@ -200,7 +198,7 @@ fun AppBarActions(
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.label_more))
|
||||
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -19,7 +18,7 @@ import androidx.compose.ui.unit.dp
|
||||
@Composable
|
||||
fun BadgeGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Row(modifier = modifier.clip(shape)) {
|
||||
|
@ -68,7 +68,7 @@ fun ChangeCategoryDialog(
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@ -78,7 +78,7 @@ private fun NotDownloadedIndicator(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(R.string.manga_download),
|
||||
modifier = Modifier.size(IndicatorSize),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@ -148,7 +148,7 @@ private fun DownloadingIndicator(
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDownward,
|
||||
imageVector = Icons.Outlined.ArrowDownward,
|
||||
contentDescription = null,
|
||||
modifier = ArrowModifier,
|
||||
tint = arrowColor,
|
||||
@ -172,7 +172,7 @@ private fun DownloadedIndicator(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(IndicatorSize),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@ -204,8 +204,8 @@ private fun ErrorIndicator(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ErrorOutline,
|
||||
contentDescription = null,
|
||||
imageVector = Icons.Outlined.ErrorOutline,
|
||||
contentDescription = stringResource(R.string.chapter_error),
|
||||
modifier = Modifier.size(IndicatorSize),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
|
@ -0,0 +1,317 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.node.modifierElementOf
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.util.selectedBackground
|
||||
|
||||
object CommonMangaItemDefaults {
|
||||
val GridHorizontalSpacer = 4.dp
|
||||
val GridVerticalSpacer = 4.dp
|
||||
|
||||
const val BrowseFavoriteCoverAlpha = 0.34f
|
||||
}
|
||||
|
||||
private const val GridSelectedCoverAlpha = 0.76f
|
||||
|
||||
/**
|
||||
* Layout of grid list item with title overlaying the cover.
|
||||
* Accepts null [title] for a cover-only view.
|
||||
*/
|
||||
@Composable
|
||||
fun MangaCompactGridItem(
|
||||
isSelected: Boolean = false,
|
||||
title: String? = null,
|
||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
GridItemSelectable(
|
||||
isSelected = isSelected,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
MangaGridCover(
|
||||
cover = {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
|
||||
data = coverData,
|
||||
)
|
||||
},
|
||||
badgesStart = coverBadgeStart,
|
||||
badgesEnd = coverBadgeEnd,
|
||||
content = {
|
||||
if (title != null) {
|
||||
CoverTextOverlay(title = title)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title overlay for [MangaCompactGridItem]
|
||||
*/
|
||||
@Composable
|
||||
private fun BoxScope.CoverTextOverlay(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color(0xAA000000),
|
||||
),
|
||||
)
|
||||
.fillMaxHeight(0.33f)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
GridItemTitle(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
title = title,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
color = Color.White,
|
||||
shadow = Shadow(
|
||||
color = Color.Black,
|
||||
blurRadius = 4f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout of grid list item with title below the cover.
|
||||
*/
|
||||
@Composable
|
||||
fun MangaComfortableGridItem(
|
||||
isSelected: Boolean = false,
|
||||
title: String,
|
||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
GridItemSelectable(
|
||||
isSelected = isSelected,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
Column {
|
||||
MangaGridCover(
|
||||
cover = {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
|
||||
data = coverData,
|
||||
)
|
||||
},
|
||||
badgesStart = coverBadgeStart,
|
||||
badgesEnd = coverBadgeEnd,
|
||||
)
|
||||
GridItemTitle(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
title = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common cover layout to add contents to be drawn on top of the cover.
|
||||
*/
|
||||
@Composable
|
||||
private fun MangaGridCover(
|
||||
modifier: Modifier = Modifier,
|
||||
cover: @Composable BoxScope.() -> Unit = {},
|
||||
badgesStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
content: @Composable (BoxScope.() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
) {
|
||||
cover()
|
||||
content?.invoke(this)
|
||||
if (badgesStart != null) {
|
||||
BadgeGroup(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
content = badgesStart,
|
||||
)
|
||||
}
|
||||
|
||||
if (badgesEnd != null) {
|
||||
BadgeGroup(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
content = badgesEnd,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GridItemTitle(
|
||||
modifier: Modifier,
|
||||
title: String,
|
||||
style: TextStyle,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = title,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 18.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for grid items to handle selection state, click and long click.
|
||||
*/
|
||||
@Composable
|
||||
private fun GridItemSelectable(
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary)
|
||||
.padding(4.dp),
|
||||
) {
|
||||
val contentColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onSecondary
|
||||
} else {
|
||||
LocalContentColor.current
|
||||
}
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see GridItemSelectable
|
||||
*/
|
||||
private fun Modifier.selectedOutline(
|
||||
isSelected: Boolean,
|
||||
color: Color,
|
||||
): Modifier {
|
||||
class SelectedOutlineNode(var selected: Boolean, var color: Color) : DrawModifierNode, Modifier.Node() {
|
||||
override fun ContentDrawScope.draw() {
|
||||
if (selected) drawRect(color)
|
||||
drawContent()
|
||||
}
|
||||
}
|
||||
|
||||
return this then modifierElementOf(
|
||||
params = isSelected.hashCode() + color.hashCode(),
|
||||
create = { SelectedOutlineNode(isSelected, color) },
|
||||
update = {
|
||||
it.selected = isSelected
|
||||
it.color = color
|
||||
},
|
||||
definitions = {
|
||||
name = "selectionOutline"
|
||||
properties["isSelected"] = isSelected
|
||||
properties["color"] = color
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout of list item.
|
||||
*/
|
||||
@Composable
|
||||
fun MangaListItem(
|
||||
isSelected: Boolean = false,
|
||||
title: String,
|
||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
badge: @Composable RowScope.() -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.selectedBackground(isSelected)
|
||||
.height(56.dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MangaCover.Square(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.alpha(coverAlpha),
|
||||
data = coverData,
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
BadgeGroup(content = badge)
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ fun DeleteLibraryMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -10,9 +10,11 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import eu.kanade.tachiyomi.R
|
||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||
|
||||
@Composable
|
||||
@ -27,7 +29,7 @@ fun DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
||||
offset = DpOffset(8.dp, (-8).dp),
|
||||
offset = DpOffset(8.dp, (-56).dp),
|
||||
properties = properties,
|
||||
content = content,
|
||||
)
|
||||
@ -46,13 +48,13 @@ fun RadioMenuItem(
|
||||
if (isChecked) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.RadioButtonChecked,
|
||||
contentDescription = "",
|
||||
contentDescription = stringResource(R.string.selected),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.RadioButtonUnchecked,
|
||||
contentDescription = "",
|
||||
contentDescription = stringResource(R.string.not_selected),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -30,7 +30,7 @@ fun DuplicateMangaDialog(
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.paddingFromBaseline
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@ -187,12 +187,12 @@ private fun WithActionPreview() {
|
||||
actions = listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_retry,
|
||||
icon = Icons.Default.Refresh,
|
||||
icon = Icons.Outlined.Refresh,
|
||||
onClick = {},
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.getting_started_guide,
|
||||
icon = Icons.Default.HelpOutline,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
|
@ -22,13 +22,13 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||
import androidx.compose.material.icons.filled.BookmarkRemove
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.RemoveDone
|
||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||
import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.RemoveDone
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -98,7 +98,7 @@ fun MangaBottomActionMenu(
|
||||
if (onBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_bookmark),
|
||||
icon = Icons.Default.BookmarkAdd,
|
||||
icon = Icons.Outlined.BookmarkAdd,
|
||||
toConfirm = confirm[0],
|
||||
onLongClick = { onLongClickItem(0) },
|
||||
onClick = onBookmarkClicked,
|
||||
@ -107,7 +107,7 @@ fun MangaBottomActionMenu(
|
||||
if (onRemoveBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_remove_bookmark),
|
||||
icon = Icons.Default.BookmarkRemove,
|
||||
icon = Icons.Outlined.BookmarkRemove,
|
||||
toConfirm = confirm[1],
|
||||
onLongClick = { onLongClickItem(1) },
|
||||
onClick = onRemoveBookmarkClicked,
|
||||
@ -116,7 +116,7 @@ fun MangaBottomActionMenu(
|
||||
if (onMarkAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_mark_as_read),
|
||||
icon = Icons.Default.DoneAll,
|
||||
icon = Icons.Outlined.DoneAll,
|
||||
toConfirm = confirm[2],
|
||||
onLongClick = { onLongClickItem(2) },
|
||||
onClick = onMarkAsReadClicked,
|
||||
@ -125,7 +125,7 @@ fun MangaBottomActionMenu(
|
||||
if (onMarkAsUnreadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_mark_as_unread),
|
||||
icon = Icons.Default.RemoveDone,
|
||||
icon = Icons.Outlined.RemoveDone,
|
||||
toConfirm = confirm[3],
|
||||
onLongClick = { onLongClickItem(3) },
|
||||
onClick = onMarkAsUnreadClicked,
|
||||
@ -254,7 +254,7 @@ fun LibraryBottomActionMenu(
|
||||
if (onMarkAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_mark_as_read),
|
||||
icon = Icons.Default.DoneAll,
|
||||
icon = Icons.Outlined.DoneAll,
|
||||
toConfirm = confirm[1],
|
||||
onLongClick = { onLongClickItem(1) },
|
||||
onClick = onMarkAsReadClicked,
|
||||
@ -263,7 +263,7 @@ fun LibraryBottomActionMenu(
|
||||
if (onMarkAsUnreadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(R.string.action_mark_as_unread),
|
||||
icon = Icons.Default.RemoveDone,
|
||||
icon = Icons.Outlined.RemoveDone,
|
||||
toConfirm = confirm[2],
|
||||
onLongClick = { onLongClickItem(2) },
|
||||
onClick = onMarkAsUnreadClicked,
|
||||
|
@ -2,7 +2,7 @@ package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -11,7 +11,6 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -26,7 +25,7 @@ enum class MangaCover(val ratio: Float) {
|
||||
modifier: Modifier = Modifier,
|
||||
data: Any?,
|
||||
contentDescription: String = "",
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
AsyncImage(
|
||||
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@ -28,7 +27,7 @@ fun Pill(
|
||||
androidx.compose.material3.Surface(
|
||||
modifier = modifier
|
||||
.padding(start = 4.dp),
|
||||
shape = RoundedCornerShape(100),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = color,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = elevation,
|
||||
|
@ -67,7 +67,7 @@ fun HistoryDeleteDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -96,7 +96,7 @@ fun HistoryDeleteAllDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ package eu.kanade.presentation.library
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -79,7 +79,7 @@ fun LibraryScreen(
|
||||
actions = listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.getting_started_guide,
|
||||
icon = Icons.Default.HelpOutline,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
|
||||
),
|
||||
),
|
||||
|
@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -26,9 +27,9 @@ fun LazyLibraryGrid(
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding + PaddingValues(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = contentPadding + PaddingValues(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
||||
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
@ -1,21 +1,14 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@Composable
|
||||
@ -44,76 +37,37 @@ fun LibraryComfortableGrid(
|
||||
items = items,
|
||||
contentType = { "library_comfortable_grid_item" },
|
||||
) { libraryItem ->
|
||||
LibraryComfortableGridItem(
|
||||
item = libraryItem,
|
||||
showDownloadBadge = showDownloadBadges,
|
||||
showUnreadBadge = showUnreadBadges,
|
||||
showLocalBadge = showLocalBadges,
|
||||
showLanguageBadge = showLanguageBadges,
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaComfortableGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverBadgeStart = {
|
||||
DownloadsBadge(
|
||||
enabled = showDownloadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
UnreadBadge(
|
||||
enabled = showUnreadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
showLanguage = showLanguageBadges,
|
||||
showLocal = showLocalBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
},
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryComfortableGridItem(
|
||||
item: LibraryItem,
|
||||
showDownloadBadge: Boolean,
|
||||
showUnreadBadge: Boolean,
|
||||
showLocalBadge: Boolean,
|
||||
showLanguageBadge: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
) {
|
||||
val libraryManga = item.libraryManga
|
||||
val manga = libraryManga.manga
|
||||
LibraryGridItemSelectable(isSelected = isSelected) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick(libraryManga)
|
||||
},
|
||||
onLongClick = {
|
||||
onLongClick(libraryManga)
|
||||
},
|
||||
),
|
||||
) {
|
||||
LibraryGridCover(
|
||||
mangaCover = MangaCover(
|
||||
manga.id,
|
||||
manga.source,
|
||||
manga.favorite,
|
||||
manga.thumbnailUrl,
|
||||
manga.coverLastModified,
|
||||
),
|
||||
item = item,
|
||||
showDownloadBadge = showDownloadBadge,
|
||||
showUnreadBadge = showUnreadBadge,
|
||||
showLocalBadge = showLocalBadge,
|
||||
showLanguageBadge = showLanguageBadge,
|
||||
)
|
||||
MangaGridComfortableText(
|
||||
text = manga.title,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaGridComfortableText(
|
||||
text: String,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
|
@ -1,35 +1,20 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@Composable
|
||||
fun LibraryCompactGrid(
|
||||
items: List<LibraryItem>,
|
||||
showTitle: Boolean,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
@ -53,92 +38,37 @@ fun LibraryCompactGrid(
|
||||
items = items,
|
||||
contentType = { "library_compact_grid_item" },
|
||||
) { libraryItem ->
|
||||
LibraryCompactGridItem(
|
||||
item = libraryItem,
|
||||
showDownloadBadge = showDownloadBadges,
|
||||
showUnreadBadge = showUnreadBadges,
|
||||
showLocalBadge = showLocalBadges,
|
||||
showLanguageBadge = showLanguageBadges,
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaCompactGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
title = manga.title.takeIf { showTitle },
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
coverBadgeStart = {
|
||||
DownloadsBadge(
|
||||
enabled = showDownloadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
UnreadBadge(
|
||||
enabled = showUnreadBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
showLanguage = showLanguageBadges,
|
||||
showLocal = showLocalBadges,
|
||||
item = libraryItem,
|
||||
)
|
||||
},
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryCompactGridItem(
|
||||
item: LibraryItem,
|
||||
showDownloadBadge: Boolean,
|
||||
showUnreadBadge: Boolean,
|
||||
showLocalBadge: Boolean,
|
||||
showLanguageBadge: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
) {
|
||||
val libraryManga = item.libraryManga
|
||||
val manga = libraryManga.manga
|
||||
LibraryGridCover(
|
||||
modifier = Modifier
|
||||
.selectedOutline(isSelected)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick(libraryManga)
|
||||
},
|
||||
onLongClick = {
|
||||
onLongClick(libraryManga)
|
||||
},
|
||||
),
|
||||
mangaCover = eu.kanade.domain.manga.model.MangaCover(
|
||||
manga.id,
|
||||
manga.source,
|
||||
manga.favorite,
|
||||
manga.thumbnailUrl,
|
||||
manga.coverLastModified,
|
||||
),
|
||||
item = item,
|
||||
showDownloadBadge = showDownloadBadge,
|
||||
showUnreadBadge = showUnreadBadge,
|
||||
showLocalBadge = showLocalBadge,
|
||||
showLanguageBadge = showLanguageBadge,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color(0xAA000000),
|
||||
),
|
||||
)
|
||||
.fillMaxHeight(0.33f)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
MangaGridCompactText(manga.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.MangaGridCompactText(
|
||||
text: String,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black,
|
||||
blurRadius = 4f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@Composable
|
||||
fun LibraryCoverOnlyGrid(
|
||||
items: List<LibraryItem>,
|
||||
showDownloadBadges: Boolean,
|
||||
showUnreadBadges: Boolean,
|
||||
showLocalBadges: Boolean,
|
||||
showLanguageBadges: Boolean,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
) {
|
||||
LazyLibraryGrid(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
globalSearchItem(searchQuery, onGlobalSearchClicked)
|
||||
|
||||
items(
|
||||
items = items,
|
||||
contentType = { "library_only_cover_grid_item" },
|
||||
) { libraryItem ->
|
||||
LibraryCoverOnlyGridItem(
|
||||
item = libraryItem,
|
||||
showDownloadBadge = showDownloadBadges,
|
||||
showUnreadBadge = showUnreadBadges,
|
||||
showLocalBadge = showLocalBadges,
|
||||
showLanguageBadge = showLanguageBadges,
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryCoverOnlyGridItem(
|
||||
item: LibraryItem,
|
||||
showDownloadBadge: Boolean,
|
||||
showUnreadBadge: Boolean,
|
||||
showLocalBadge: Boolean,
|
||||
showLanguageBadge: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
) {
|
||||
val libraryManga = item.libraryManga
|
||||
val manga = libraryManga.manga
|
||||
LibraryGridCover(
|
||||
modifier = Modifier
|
||||
.selectedOutline(isSelected)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick(libraryManga)
|
||||
},
|
||||
onLongClick = {
|
||||
onLongClick(libraryManga)
|
||||
},
|
||||
),
|
||||
mangaCover = eu.kanade.domain.manga.model.MangaCover(
|
||||
manga.id,
|
||||
manga.source,
|
||||
manga.favorite,
|
||||
manga.thumbnailUrl,
|
||||
manga.coverLastModified,
|
||||
),
|
||||
item = item,
|
||||
showDownloadBadge = showDownloadBadge,
|
||||
showUnreadBadge = showUnreadBadge,
|
||||
showLocalBadge = showLocalBadge,
|
||||
showLanguageBadge = showLanguageBadge,
|
||||
)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.BadgeGroup
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@Composable
|
||||
fun MangaGridCover(
|
||||
modifier: Modifier = Modifier,
|
||||
cover: @Composable BoxScope.() -> Unit = {},
|
||||
badgesStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
) {
|
||||
cover()
|
||||
content()
|
||||
if (badgesStart != null) {
|
||||
BadgeGroup(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
content = badgesStart,
|
||||
)
|
||||
}
|
||||
|
||||
if (badgesEnd != null) {
|
||||
BadgeGroup(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
content = badgesEnd,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryGridCover(
|
||||
modifier: Modifier = Modifier,
|
||||
mangaCover: eu.kanade.domain.manga.model.MangaCover,
|
||||
item: LibraryItem,
|
||||
showDownloadBadge: Boolean,
|
||||
showUnreadBadge: Boolean,
|
||||
showLocalBadge: Boolean,
|
||||
showLanguageBadge: Boolean,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
MangaGridCover(
|
||||
modifier = modifier,
|
||||
cover = {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
data = mangaCover,
|
||||
)
|
||||
},
|
||||
badgesStart = {
|
||||
DownloadsBadge(enabled = showDownloadBadge, item = item)
|
||||
UnreadBadge(enabled = showUnreadBadge, item = item)
|
||||
},
|
||||
badgesEnd = {
|
||||
LanguageBadge(showLanguage = showLanguageBadge, showLocal = showLocalBadge, item = item)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun Modifier.selectedOutline(isSelected: Boolean) = composed {
|
||||
val secondary = MaterialTheme.colorScheme.secondary
|
||||
if (isSelected) {
|
||||
drawBehind {
|
||||
val additional = 24.dp.value
|
||||
val offset = additional / 2
|
||||
val height = size.height + additional
|
||||
val width = size.width + additional
|
||||
drawRoundRect(
|
||||
color = secondary,
|
||||
topLeft = Offset(-offset, -offset),
|
||||
size = Size(width, height),
|
||||
cornerRadius = CornerRadius(offset),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryGridItemSelectable(
|
||||
isSelected: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(Modifier.selectedOutline(isSelected)) {
|
||||
CompositionLocalProvider(LocalContentColor provides if (isSelected) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onBackground) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,21 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.zIndex
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.presentation.components.BadgeGroup
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.MangaCover.Square
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.selectedBackground
|
||||
import eu.kanade.presentation.util.verticalPadding
|
||||
import eu.kanade.presentation.components.MangaListItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
|
||||
@ -47,7 +35,7 @@ fun LibraryList(
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
item {
|
||||
if (searchQuery.isNullOrEmpty().not()) {
|
||||
@ -64,116 +52,25 @@ fun LibraryList(
|
||||
items = items,
|
||||
contentType = { "library_list_item" },
|
||||
) { libraryItem ->
|
||||
LibraryListItem(
|
||||
item = libraryItem,
|
||||
showDownloadBadge = showDownloadBadges,
|
||||
showUnreadBadge = showUnreadBadges,
|
||||
showLocalBadge = showLocalBadges,
|
||||
showLanguageBadge = showLanguageBadges,
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaListItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
sourceId = manga.source,
|
||||
isMangaFavorite = manga.favorite,
|
||||
url = manga.thumbnailUrl,
|
||||
lastModified = manga.coverLastModified,
|
||||
),
|
||||
badge = {
|
||||
DownloadsBadge(enabled = showDownloadBadges, item = libraryItem)
|
||||
UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
|
||||
LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
|
||||
},
|
||||
onLongClick = { onLongClick(libraryItem.libraryManga) },
|
||||
onClick = { onClick(libraryItem.libraryManga) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryListItem(
|
||||
item: LibraryItem,
|
||||
showDownloadBadge: Boolean,
|
||||
showUnreadBadge: Boolean,
|
||||
showLocalBadge: Boolean,
|
||||
showLanguageBadge: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
) {
|
||||
val libraryManga = item.libraryManga
|
||||
val manga = libraryManga.manga
|
||||
MangaListItem(
|
||||
modifier = Modifier.selectedBackground(isSelected),
|
||||
title = manga.title,
|
||||
cover = MangaCover(
|
||||
manga.id,
|
||||
manga.source,
|
||||
manga.favorite,
|
||||
manga.thumbnailUrl,
|
||||
manga.coverLastModified,
|
||||
),
|
||||
onClick = { onClick(libraryManga) },
|
||||
onLongClick = { onLongClick(libraryManga) },
|
||||
) {
|
||||
DownloadsBadge(enabled = showDownloadBadge, item = item)
|
||||
UnreadBadge(enabled = showUnreadBadge, item = item)
|
||||
LanguageBadge(showLanguage = showLanguageBadge, showLocal = showLocalBadge, item = item)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
cover: MangaCover,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit = onClick,
|
||||
badges: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
MangaListItem(
|
||||
modifier = modifier,
|
||||
coverContent = {
|
||||
Square(
|
||||
modifier = Modifier
|
||||
.padding(vertical = verticalPadding)
|
||||
.fillMaxHeight(),
|
||||
data = cover,
|
||||
)
|
||||
},
|
||||
badges = badges,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
content = {
|
||||
MangaListItemContent(title)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
coverContent: @Composable RowScope.() -> Unit,
|
||||
badges: @Composable RowScope.() -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(56.dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(horizontal = horizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
coverContent()
|
||||
content()
|
||||
BadgeGroup(content = badges)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.MangaListItemContent(
|
||||
text: String,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
@ -72,9 +72,10 @@ fun LibraryPager(
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
)
|
||||
}
|
||||
LibraryDisplayMode.CompactGrid -> {
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
LibraryCompactGrid(
|
||||
items = library,
|
||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
@ -104,22 +105,6 @@ fun LibraryPager(
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
)
|
||||
}
|
||||
LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
LibraryCoverOnlyGrid(
|
||||
items = library,
|
||||
showDownloadBadges = showDownloadBadges,
|
||||
showUnreadBadges = showUnreadBadges,
|
||||
showLocalBadges = showLocalBadges,
|
||||
showLanguageBadges = showLanguageBadges,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
searchQuery = searchQuery,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,10 +150,10 @@ fun LibrarySelectionToolbar(
|
||||
titleContent = { Text(text = "${state.selection.size}") },
|
||||
actions = {
|
||||
IconButton(onClick = onClickSelectAll) {
|
||||
Icon(Icons.Outlined.SelectAll, contentDescription = "search")
|
||||
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
|
||||
}
|
||||
IconButton(onClick = onClickInvertSelection) {
|
||||
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert")
|
||||
Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
|
||||
}
|
||||
},
|
||||
isActionMode = true,
|
||||
|
@ -273,7 +273,7 @@ private fun MangaScreenSmallImpl(
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
@ -486,7 +486,7 @@ fun MangaScreenLargeImpl(
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
|
@ -37,7 +37,7 @@ fun DownloadCustomAmountDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -62,13 +62,13 @@ fun DownloadCustomAmountDialog(
|
||||
onClick = { setAmount(amount - 10) },
|
||||
enabled = amount > 0,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "-10")
|
||||
}
|
||||
IconButton(
|
||||
onClick = { setAmount(amount - 1) },
|
||||
enabled = amount > 0,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "-1")
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.weight(1f),
|
||||
@ -81,13 +81,13 @@ fun DownloadCustomAmountDialog(
|
||||
onClick = { setAmount(amount + 1) },
|
||||
enabled = amount < maxAmount,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "+1")
|
||||
}
|
||||
IconButton(
|
||||
onClick = { setAmount(amount + 10) },
|
||||
enabled = amount < maxAmount,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "+10")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ fun MangaChapterListItem(
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
|
@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
@ -63,7 +63,7 @@ fun MangaCoverDialog(
|
||||
) {
|
||||
IconButton(onClick = onDismissRequest) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_close),
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ fun DeleteChaptersDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -23,18 +23,18 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachMoney
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
@ -173,7 +173,7 @@ fun MangaActionRow(
|
||||
} else {
|
||||
stringResource(R.string.add_to_library)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
@ -185,7 +185,7 @@ fun MangaActionRow(
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
@ -193,7 +193,7 @@ fun MangaActionRow(
|
||||
if (onWebViewClicked != null) {
|
||||
MangaActionButton(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
icon = Icons.Outlined.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
@ -345,13 +345,13 @@ private fun MangaAndSourceTitlesLarge(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
@ -375,7 +375,7 @@ private fun MangaAndSourceTitlesLarge(
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
@ -478,13 +478,13 @@ private fun MangaAndSourceTitlesSmall(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
@ -508,7 +508,7 @@ private fun MangaAndSourceTitlesSmall(
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
|
@ -5,13 +5,13 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -71,7 +71,7 @@ fun MangaToolbar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Icon(
|
||||
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
|
||||
imageVector = if (isActionMode) Icons.Outlined.Close else Icons.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
@ -80,13 +80,13 @@ fun MangaToolbar(
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
imageVector = Icons.Outlined.SelectAll,
|
||||
contentDescription = stringResource(R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
imageVector = Icons.Outlined.FlipToBack,
|
||||
contentDescription = stringResource(R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
@ -161,7 +161,7 @@ fun MangaToolbar(
|
||||
Box {
|
||||
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ internal fun PreferenceItem(
|
||||
ListPreferenceWidget(
|
||||
value = value,
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
subtitle = item.internalSubtitleProvider(value, item.entries),
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValueChange = { newValue ->
|
||||
@ -98,7 +98,7 @@ internal fun PreferenceItem(
|
||||
ListPreferenceWidget(
|
||||
value = item.value,
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
subtitle = item.subtitleProvider(item.value, item.entries),
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
||||
|
@ -1,7 +1,11 @@
|
||||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData
|
||||
|
||||
@ -47,6 +51,8 @@ sealed class Preference {
|
||||
val pref: PreferenceData<T>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
@ -55,6 +61,10 @@ sealed class Preference {
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
|
||||
subtitleProvider(value as T, entries as Map<T, String>)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,6 +74,8 @@ sealed class Preference {
|
||||
val value: String,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
@ -78,7 +90,15 @@ sealed class Preference {
|
||||
data class MultiSelectListPreference(
|
||||
val pref: PreferenceData<Set<String>>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
|
||||
val combined = remember(v) {
|
||||
v.map { e[it] }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.joinToString()
|
||||
} ?: stringResource(id = R.string.none)
|
||||
subtitle?.format(combined)
|
||||
},
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.presentation.more.settings
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
@ -28,7 +28,7 @@ fun PreferenceScaffold(
|
||||
if (onBackPressed != null) {
|
||||
IconButton(onClick = onBackPressed) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
imageVector = Icons.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ class ClearDatabaseScreen : Screen {
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = model::hideConfirmation) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
|
@ -345,7 +345,7 @@ class SettingsAdvancedScreen : SearchableSettings {
|
||||
text = { Text(text = stringResource(R.string.ext_installer_shizuku_unavailable_dialog)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = dismiss) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -72,7 +72,6 @@ class SettingsAppearanceScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = themeModePref,
|
||||
title = stringResource(R.string.pref_theme_mode),
|
||||
subtitle = "%s",
|
||||
entries = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mapOf(
|
||||
ThemeMode.SYSTEM to stringResource(R.string.theme_system),
|
||||
@ -129,7 +128,6 @@ class SettingsAppearanceScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
title = stringResource(R.string.pref_relative_format),
|
||||
subtitle = "%s",
|
||||
entries = mapOf(
|
||||
0 to stringResource(R.string.off),
|
||||
2 to stringResource(R.string.pref_relative_time_short),
|
||||
@ -139,7 +137,6 @@ class SettingsAppearanceScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(R.string.pref_date_format),
|
||||
subtitle = "%s",
|
||||
entries = DateFormats
|
||||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
|
@ -192,7 +192,7 @@ class SettingsBackupScreen : SearchableSettings {
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -252,7 +252,7 @@ class SettingsBackupScreen : SearchableSettings {
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.copy))
|
||||
Text(text = stringResource(android.R.string.copy))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -27,7 +26,6 @@ import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -101,9 +99,11 @@ class SettingsDownloadScreen : SearchableSettings {
|
||||
return Preference.PreferenceItem.ListPreference(
|
||||
pref = currentDirPref,
|
||||
title = stringResource(R.string.pref_download_directory),
|
||||
subtitle = remember(currentDir) {
|
||||
UniFile.fromUri(context, currentDir.toUri())?.filePath
|
||||
} ?: stringResource(R.string.invalid_location, currentDir),
|
||||
subtitleProvider = { value, _ ->
|
||||
remember(value) {
|
||||
UniFile.fromUri(context, value.toUri())?.filePath
|
||||
} ?: stringResource(R.string.invalid_location, value)
|
||||
},
|
||||
entries = mapOf(
|
||||
defaultDirPair,
|
||||
customDirEntryKey to stringResource(R.string.custom_dir),
|
||||
@ -173,25 +173,10 @@ class SettingsDownloadScreen : SearchableSettings {
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
val none = stringResource(R.string.none)
|
||||
val pref = downloadPreferences.removeExcludeCategories()
|
||||
val entries = categories().associate { it.id.toString() to it.visualName }
|
||||
val subtitle by produceState(initialValue = "") {
|
||||
pref.changes()
|
||||
.stateIn(this)
|
||||
.collect { mutable ->
|
||||
value = mutable
|
||||
.mapNotNull { id -> entries[id] }
|
||||
.sortedBy { entries.values.indexOf(it) }
|
||||
.joinToString()
|
||||
.ifEmpty { none }
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = pref,
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(R.string.pref_remove_exclude_categories),
|
||||
subtitle = subtitle,
|
||||
entries = entries,
|
||||
entries = categories().associate { it.id.toString() to it.visualName },
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,6 @@ class SettingsGeneralScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.BasicListPreference(
|
||||
value = currentLanguage,
|
||||
title = stringResource(R.string.pref_app_language),
|
||||
subtitle = "%s",
|
||||
entries = langs,
|
||||
onValueChanged = { newValue ->
|
||||
currentLanguage = newValue
|
||||
|
@ -177,28 +177,6 @@ class SettingsLibraryScreen : SearchableSettings {
|
||||
|
||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
||||
|
||||
val deviceRestrictionEntries = mapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(R.string.charging),
|
||||
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
|
||||
)
|
||||
val deviceRestrictions = libraryUpdateDeviceRestrictionPref.collectAsState()
|
||||
.value
|
||||
.sorted()
|
||||
.map { deviceRestrictionEntries.getOrElse(it) { it } }
|
||||
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
|
||||
|
||||
val mangaRestrictionEntries = mapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||
)
|
||||
val mangaRestrictions = libraryUpdateMangaRestrictionPref.collectAsState()
|
||||
.value
|
||||
.map { mangaRestrictionEntries.getOrElse(it) { it } }
|
||||
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
|
||||
|
||||
val included by libraryUpdateCategoriesPref.collectAsState()
|
||||
val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
@ -224,7 +202,6 @@ class SettingsLibraryScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryUpdateIntervalPref,
|
||||
title = stringResource(R.string.pref_library_update_interval),
|
||||
subtitle = "%s",
|
||||
entries = mapOf(
|
||||
0 to stringResource(R.string.update_never),
|
||||
12 to stringResource(R.string.update_12hour),
|
||||
@ -242,8 +219,13 @@ class SettingsLibraryScreen : SearchableSettings {
|
||||
pref = libraryUpdateDeviceRestrictionPref,
|
||||
enabled = libraryUpdateInterval > 0,
|
||||
title = stringResource(R.string.pref_library_update_restriction),
|
||||
subtitle = stringResource(R.string.restrictions, deviceRestrictions),
|
||||
entries = deviceRestrictionEntries,
|
||||
subtitle = stringResource(R.string.restrictions),
|
||||
entries = mapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(R.string.charging),
|
||||
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
|
||||
),
|
||||
onValueChanged = {
|
||||
// Post to event looper to allow the preference to be updated.
|
||||
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
|
||||
@ -253,8 +235,11 @@ class SettingsLibraryScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateMangaRestrictionPref,
|
||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||
subtitle = mangaRestrictions,
|
||||
entries = mangaRestrictionEntries,
|
||||
entries = mapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.categories),
|
||||
@ -341,7 +326,7 @@ class SettingsLibraryScreen : SearchableSettings {
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -8,7 +8,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.ChromeReaderMode
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
@ -98,7 +98,7 @@ object SettingsMainScreen : Screen {
|
||||
navigationIcon = {
|
||||
IconButton(onClick = backPress::invoke) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
imageVector = Icons.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -95,8 +95,8 @@ class SettingsSearchScreen : Screen {
|
||||
if (canPop) {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
imageVector = Icons.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
@ -131,7 +131,7 @@ class SettingsSearchScreen : Screen {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
IconButton(onClick = { textFieldValue = TextFieldValue() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
@ -49,7 +49,6 @@ class SettingsSecurityScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.lockAppAfter(),
|
||||
title = stringResource(R.string.lock_when_idle),
|
||||
subtitle = "%s",
|
||||
enabled = authSupported && useAuth,
|
||||
entries = LockAfterValues
|
||||
.associateWith {
|
||||
@ -72,7 +71,6 @@ class SettingsSecurityScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.secureScreen(),
|
||||
title = stringResource(R.string.secure_screen),
|
||||
subtitle = "%s",
|
||||
entries = SecurityPreferences.SecureScreenMode.values()
|
||||
.associateWith { stringResource(it.titleResId) },
|
||||
),
|
||||
|
@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@ -71,7 +71,7 @@ class SettingsTrackingScreen : SearchableSettings {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.HelpOutline,
|
||||
imageVector = Icons.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(R.string.tracking_guide),
|
||||
)
|
||||
}
|
||||
@ -199,7 +199,7 @@ class SettingsTrackingScreen : SearchableSettings {
|
||||
)
|
||||
IconButton(onClick = onDismissRequest) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_close),
|
||||
)
|
||||
}
|
||||
@ -227,9 +227,9 @@ class SettingsTrackingScreen : SearchableSettings {
|
||||
IconButton(onClick = { hidePassword = !hidePassword }) {
|
||||
Icon(
|
||||
imageVector = if (hidePassword) {
|
||||
Icons.Default.Visibility
|
||||
Icons.Filled.Visibility
|
||||
} else {
|
||||
Icons.Default.VisibilityOff
|
||||
Icons.Filled.VisibilityOff
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
@ -317,7 +317,7 @@ class SettingsTrackingScreen : SearchableSettings {
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
@ -45,6 +45,7 @@ import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
|
||||
@ -158,7 +159,7 @@ fun AppThemePreviewItem(
|
||||
.padding(end = 4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
),
|
||||
)
|
||||
|
||||
@ -168,8 +169,8 @@ fun AppThemePreviewItem(
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.selected),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@ -182,7 +183,7 @@ fun AppThemePreviewItem(
|
||||
.padding(start = 8.dp, top = 2.dp)
|
||||
.background(
|
||||
color = dividerColor,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
)
|
||||
.fillMaxWidth(0.5f)
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
@ -242,7 +243,7 @@ fun AppThemePreviewItem(
|
||||
.weight(1f)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@ -71,7 +72,7 @@ fun EditTextPreferenceWidget(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
@ -26,6 +25,7 @@ import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.util.isScrolledToEnd
|
||||
import eu.kanade.presentation.util.isScrolledToStart
|
||||
import eu.kanade.presentation.util.minimumTouchTargetSize
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun <T> ListPreferenceWidget(
|
||||
@ -40,7 +40,7 @@ fun <T> ListPreferenceWidget(
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = title,
|
||||
subtitle = subtitle?.format(entries[value]),
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
onPreferenceClick = { showDialog(true) },
|
||||
)
|
||||
@ -73,7 +73,7 @@ fun <T> ListPreferenceWidget(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDialog(false) }) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -89,7 +89,7 @@ private fun DialogRow(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { if (!isSelected) onSelected() },
|
||||
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -23,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.minimumTouchTargetSize
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun MultiSelectListPreferenceWidget(
|
||||
@ -34,7 +34,7 @@ fun MultiSelectListPreferenceWidget(
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = preference.title,
|
||||
subtitle = preference.subtitle,
|
||||
subtitle = preference.subtitleProvider(values, preference.entries),
|
||||
icon = preference.icon,
|
||||
onPreferenceClick = { showDialog(true) },
|
||||
)
|
||||
@ -62,7 +62,7 @@ fun MultiSelectListPreferenceWidget(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onSelectionChanged() },
|
||||
@ -99,7 +99,7 @@ fun MultiSelectListPreferenceWidget(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDialog(false) }) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -44,7 +44,7 @@ private fun SwitchPreferenceWidgetPreview() {
|
||||
SwitchPreferenceWidget(
|
||||
title = "Text preference with icon",
|
||||
subtitle = "Text preference summary",
|
||||
icon = Icons.Default.Preview,
|
||||
icon = Icons.Filled.Preview,
|
||||
checked = true,
|
||||
onCheckedChanged = {},
|
||||
)
|
||||
|
@ -67,7 +67,7 @@ private fun TextPreferenceWidgetPreview() {
|
||||
TextPreferenceWidget(
|
||||
title = "Text preference with icon",
|
||||
subtitle = "Text preference summary",
|
||||
icon = Icons.Default.Preview,
|
||||
icon = Icons.Filled.Preview,
|
||||
onPreferenceClick = {},
|
||||
)
|
||||
TextPreferenceWidget(
|
||||
|
@ -10,9 +10,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@ -21,8 +20,10 @@ 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
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun TrackingPreferenceWidget(
|
||||
@ -45,7 +46,7 @@ fun TrackingPreferenceWidget(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(logoColor), shape = RoundedCornerShape(8.dp))
|
||||
.background(color = Color(logoColor), shape = MaterialTheme.shapes.small)
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@ -65,12 +66,12 @@ fun TrackingPreferenceWidget(
|
||||
)
|
||||
if (checked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
imageVector = Icons.Outlined.Done,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.size(32.dp),
|
||||
tint = Color(0xFF4CAF50),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(R.string.login_success),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.CheckBox
|
||||
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
|
||||
@ -79,7 +78,7 @@ fun <T> TriStateListDialog(
|
||||
val state = selected[index]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable {
|
||||
selected[index] = when (state) {
|
||||
State.UNCHECKED -> State.CHECKED
|
||||
@ -103,7 +102,13 @@ fun <T> TriStateListDialog(
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(
|
||||
when (state) {
|
||||
State.UNCHECKED -> R.string.not_selected
|
||||
State.CHECKED -> R.string.selected
|
||||
State.INVERSED -> R.string.disabled
|
||||
},
|
||||
),
|
||||
)
|
||||
Text(text = itemLabel(item))
|
||||
}
|
||||
@ -117,7 +122,7 @@ fun <T> TriStateListDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -27,7 +27,7 @@ fun UpdatesDeleteConfirmationDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@ -215,7 +215,7 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = onUpdateLibrary) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.action_update_library),
|
||||
)
|
||||
}
|
||||
@ -225,13 +225,13 @@ private fun UpdatesAppBar(
|
||||
actionModeActions = {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
imageVector = Icons.Outlined.SelectAll,
|
||||
contentDescription = stringResource(R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
imageVector = Icons.Outlined.FlipToBack,
|
||||
contentDescription = stringResource(R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ fun UpdatesUiItem(
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
|
@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@ -48,13 +48,13 @@ fun WebViewScreen(
|
||||
title = state.pageTitle ?: initialTitle,
|
||||
subtitle = state.content.getCurrentUrl(),
|
||||
navigateUp = onNavigateUp,
|
||||
navigationIcon = Icons.Default.Close,
|
||||
navigationIcon = Icons.Outlined.Close,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_webview_back),
|
||||
icon = Icons.Default.ArrowBack,
|
||||
icon = Icons.Outlined.ArrowBack,
|
||||
onClick = {
|
||||
if (navigator.canGoBack) {
|
||||
navigator.navigateBack()
|
||||
@ -64,7 +64,7 @@ fun WebViewScreen(
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_webview_forward),
|
||||
icon = Icons.Default.ArrowForward,
|
||||
icon = Icons.Outlined.ArrowForward,
|
||||
onClick = {
|
||||
if (navigator.canGoForward) {
|
||||
navigator.navigateForward()
|
||||
|
@ -48,6 +48,8 @@ class DownloadCache(
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val notifier by lazy { DownloadNotifier(context) }
|
||||
|
||||
/**
|
||||
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
|
||||
* issues, as the cache is only used for UI feedback.
|
||||
@ -241,56 +243,62 @@ class DownloadCache(
|
||||
}
|
||||
|
||||
renewalJob = scope.launchIO {
|
||||
var sources = getSources()
|
||||
try {
|
||||
notifier.onCacheProgress()
|
||||
|
||||
// Try to wait until extensions and sources have loaded
|
||||
withTimeout(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
var sources = getSources()
|
||||
|
||||
while (sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
}
|
||||
}
|
||||
// Try to wait until extensions and sources have loaded
|
||||
withTimeout(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
|
||||
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
||||
.associate { it.name to SourceDirectory(it) }
|
||||
.mapNotNullKeys { entry ->
|
||||
sources.find {
|
||||
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
|
||||
}?.id
|
||||
}
|
||||
|
||||
rootDownloadsDir.sourceDirs = sourceDirs
|
||||
|
||||
sourceDirs.values
|
||||
.map { sourceDir ->
|
||||
async {
|
||||
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
||||
.filterNot { it.name.isNullOrBlank() }
|
||||
.associate { it.name!! to MangaDirectory(it) }
|
||||
|
||||
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
|
||||
|
||||
mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
|
||||
.mapNotNull { chapterDir ->
|
||||
chapterDir.name
|
||||
?.replace(".cbz", "")
|
||||
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||
}
|
||||
.toMutableSet()
|
||||
|
||||
mangaDir.chapterDirs = chapterDirs
|
||||
}
|
||||
while (sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
lastRenew = System.currentTimeMillis()
|
||||
notifyChanges()
|
||||
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
||||
.associate { it.name to SourceDirectory(it) }
|
||||
.mapNotNullKeys { entry ->
|
||||
sources.find {
|
||||
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
|
||||
}?.id
|
||||
}
|
||||
|
||||
rootDownloadsDir.sourceDirs = sourceDirs
|
||||
|
||||
sourceDirs.values
|
||||
.map { sourceDir ->
|
||||
async {
|
||||
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
||||
.filterNot { it.name.isNullOrBlank() }
|
||||
.associate { it.name!! to MangaDirectory(it) }
|
||||
|
||||
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
|
||||
|
||||
mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
|
||||
.mapNotNull { chapterDir ->
|
||||
chapterDir.name
|
||||
?.replace(".cbz", "")
|
||||
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||
}
|
||||
.toMutableSet()
|
||||
|
||||
mangaDir.chapterDirs = chapterDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
lastRenew = System.currentTimeMillis()
|
||||
notifyChanges()
|
||||
} finally {
|
||||
notifier.dismissCacheProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,17 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val cacheNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_CACHE) {
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setContentTitle(context.getString(R.string.download_notifier_cache_renewal))
|
||||
setProgress(100, 100, true)
|
||||
setOngoing(true)
|
||||
setAutoCancel(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of download. Used for correct notification icon.
|
||||
*/
|
||||
@ -233,4 +244,14 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
errorThrown = true
|
||||
isDownloading = false
|
||||
}
|
||||
|
||||
fun onCacheProgress() {
|
||||
with(cacheNotificationBuilder) {
|
||||
show(Notifications.ID_DOWNLOAD_CACHE)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissCacheProgress() {
|
||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CACHE)
|
||||
}
|
||||
}
|
||||
|
@ -485,17 +485,15 @@ class Downloader(
|
||||
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
|
||||
if (!downloadPreferences.splitTallImages().get()) return true
|
||||
|
||||
val filename = String.format("%03d", page.number)
|
||||
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
|
||||
val filenamePrefix = String.format("%03d", page.number)
|
||||
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
|
||||
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
|
||||
val imageFilePath = imageFile.filePath
|
||||
?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
|
||||
|
||||
// check if the original page was previously splitted before then skip.
|
||||
if (imageFile.name!!.contains("__")) return true
|
||||
|
||||
return try {
|
||||
ImageUtil.splitTallImage(imageFile, imageFilePath)
|
||||
ImageUtil.splitTallImage(tmpDir, imageFile, filenamePrefix)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
|
@ -59,7 +59,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
setLargeIcon(notificationBitmap)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
|
||||
addAction(R.drawable.ic_close_24dp, context.getString(R.string.action_cancel), cancelIntent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,8 @@ object Notifications {
|
||||
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||
const val CHANNEL_DOWNLOADER_CACHE = "downloader_cache_renewal"
|
||||
const val ID_DOWNLOAD_CACHE = -204
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the library updater.
|
||||
@ -159,6 +161,11 @@ object Notifications {
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_CACHE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_downloader_cache))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
|
@ -180,8 +180,7 @@ class ExtensionManager(
|
||||
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
|
||||
changed = true
|
||||
} else if (availableExt != null) {
|
||||
val hasUpdate = !installedExt.isUnofficial &&
|
||||
availableExt.versionCode > installedExt.versionCode
|
||||
val hasUpdate = installedExt.updateExists(availableExt)
|
||||
|
||||
if (installedExt.hasUpdate != hasUpdate) {
|
||||
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
|
||||
@ -347,11 +346,18 @@ class ExtensionManager(
|
||||
* Extension method to set the update field of an installed extension.
|
||||
*/
|
||||
private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
|
||||
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||
if (!isUnofficial && availableExt != null && availableExt.versionCode > versionCode) {
|
||||
return copy(hasUpdate = true)
|
||||
return if (updateExists()) {
|
||||
copy(hasUpdate = true)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||
if (isUnofficial || availableExt == null) return false
|
||||
|
||||
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
||||
}
|
||||
|
||||
private fun updatePendingUpdatesCount() {
|
||||
|
@ -100,7 +100,7 @@ internal class ExtensionGithubApi {
|
||||
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||
val libVersion = it.extractLibVersion()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map {
|
||||
@ -109,6 +109,7 @@ internal class ExtensionGithubApi {
|
||||
pkgName = it.pkg,
|
||||
versionName = it.version,
|
||||
versionCode = it.code,
|
||||
libVersion = it.extractLibVersion(),
|
||||
lang = it.lang,
|
||||
isNsfw = it.nsfw == 1,
|
||||
hasReadme = it.hasReadme == 1,
|
||||
@ -142,6 +143,10 @@ internal class ExtensionGithubApi {
|
||||
REPO_URL_PREFIX
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
||||
return version.substringBeforeLast('.').toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
|
@ -10,6 +10,7 @@ sealed class Extension {
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
abstract val libVersion: Double
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
abstract val hasReadme: Boolean
|
||||
@ -20,6 +21,7 @@ sealed class Extension {
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
override val hasReadme: Boolean,
|
||||
@ -37,6 +39,7 @@ sealed class Extension {
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
override val hasReadme: Boolean,
|
||||
@ -51,6 +54,7 @@ sealed class Extension {
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false,
|
||||
|
@ -141,7 +141,7 @@ internal object ExtensionLoader {
|
||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||
return LoadResult.Error
|
||||
} else if (signatureHash !in trustedSignatures) {
|
||||
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
||||
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
||||
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||
return LoadResult.Untrusted(extension)
|
||||
}
|
||||
@ -190,14 +190,15 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val extension = Extension.Installed(
|
||||
extName,
|
||||
pkgName,
|
||||
versionName,
|
||||
versionCode,
|
||||
lang,
|
||||
isNsfw,
|
||||
hasReadme,
|
||||
hasChangelog,
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
libVersion = libVersion,
|
||||
lang = lang,
|
||||
isNsfw = isNsfw,
|
||||
hasReadme = hasReadme,
|
||||
hasChangelog = hasChangelog,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
isUnofficial = signatureHash != officialSignature,
|
||||
|
@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SourceManager(
|
||||
private val context: Context,
|
||||
@ -31,7 +32,7 @@ class SourceManager(
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private var sourcesMap = emptyMap<Long, Source>()
|
||||
private var sourcesMap = ConcurrentHashMap<Long, Source>()
|
||||
set(value) {
|
||||
field = value
|
||||
sourcesMapFlow.value = field
|
||||
@ -39,7 +40,7 @@ class SourceManager(
|
||||
|
||||
private val sourcesMapFlow = MutableStateFlow(sourcesMap)
|
||||
|
||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
|
||||
|
||||
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
|
||||
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { sources -> sources.filterIsInstance<HttpSource>() }
|
||||
@ -48,7 +49,7 @@ class SourceManager(
|
||||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = mutableMapOf<Long, Source>(LocalSource.ID to LocalSource(context))
|
||||
val mutableMap = ConcurrentHashMap<Long, Source>(mapOf(LocalSource.ID to LocalSource(context)))
|
||||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
|
@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -35,6 +36,7 @@ class ExtensionFilterPresenter(
|
||||
logcat(LogPriority.ERROR, exception)
|
||||
_events.send(Event.FailedFetchingLanguages)
|
||||
}
|
||||
.stateIn(presenterScope)
|
||||
.collectLatest(::collectLatestSourceLangMap)
|
||||
}
|
||||
}
|
||||
|
@ -29,10 +29,6 @@ class SourceSearchController(
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
// LocalContext is not a first available to us when we try access it
|
||||
// Decoupling from BrowseSourceController is needed
|
||||
val context = applicationContext!!
|
||||
|
||||
SourceSearchScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
@ -46,8 +42,10 @@ class SourceSearchController(
|
||||
},
|
||||
onWebViewClick = f@{
|
||||
val source = presenter.source as? HttpSource ?: return@f
|
||||
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
|
||||
context.startActivity(intent)
|
||||
activity?.let { context ->
|
||||
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -39,6 +40,7 @@ class SourcesFilterPresenter(
|
||||
logcat(LogPriority.ERROR, exception)
|
||||
_events.send(Event.FailedFetchingLanguages)
|
||||
}
|
||||
.stateIn(presenterScope)
|
||||
.collectLatest(::collectLatestSourceLangMap)
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -152,7 +152,7 @@ class DownloadController :
|
||||
IconButton(onClick = { onExpanded(!expanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(R.string.label_more),
|
||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
CascadeDropdownMenu(
|
||||
@ -234,9 +234,9 @@ class DownloadController :
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Default.Pause
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Default.PlayArrow
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
@ -126,7 +127,6 @@ class LibraryController(
|
||||
settingsSheet = LibrarySettingsSheet(router) { group ->
|
||||
when (group) {
|
||||
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
|
||||
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
||||
else -> {} // Handled via different mechanisms
|
||||
}
|
||||
}
|
||||
@ -152,12 +152,10 @@ class LibraryController(
|
||||
}
|
||||
|
||||
private fun onFilterChanged() {
|
||||
presenter.requestFilterUpdate()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun onSortChanged() {
|
||||
presenter.requestSortUpdate()
|
||||
viewScope.launchUI {
|
||||
presenter.requestFilterUpdate()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
@ -180,7 +178,7 @@ class LibraryController(
|
||||
* Clear all of the manga currently selected, and
|
||||
* invalidate the action mode to revert the top toolbar
|
||||
*/
|
||||
fun clearSelection() {
|
||||
private fun clearSelection() {
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.core.prefs.CheckboxState
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.core.util.asObservable
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
@ -32,7 +29,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.library.LibraryState
|
||||
import eu.kanade.presentation.library.LibraryStateImpl
|
||||
@ -49,16 +46,16 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.Collator
|
||||
@ -79,7 +76,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
|
||||
class LibraryPresenter(
|
||||
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
|
||||
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
@ -111,15 +108,8 @@ class LibraryPresenter(
|
||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE)
|
||||
private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
|
||||
|
||||
private var librarySubscription: Job? = null
|
||||
|
||||
@ -129,30 +119,23 @@ class LibraryPresenter(
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
/**
|
||||
* TODO: Move this to a coroutine world
|
||||
* TODO:
|
||||
* - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
|
||||
* - Create new db view and new query to just fetch the current category save as needed to instance variable
|
||||
* - Fetch badges to maps and retrieve as needed instead of fetching all of them at once
|
||||
*/
|
||||
if (librarySubscription == null || librarySubscription!!.isCancelled) {
|
||||
librarySubscription = presenterScope.launchIO {
|
||||
getLibraryFlow().asObservable()
|
||||
.combineLatest(getFilterObservable()) { lib, tracks ->
|
||||
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
|
||||
}
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.asFlow()
|
||||
combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
|
||||
library.mangaMap
|
||||
.applyFilters(tracks)
|
||||
.applySort(library.categories)
|
||||
}
|
||||
.collectLatest {
|
||||
state.isLoading = false
|
||||
loadedManga = it.mangaMap
|
||||
loadedManga = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,21 +143,24 @@ class LibraryPresenter(
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Long, Boolean>>): LibraryMap {
|
||||
private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap {
|
||||
val downloadedOnly = preferences.downloadedOnly().get()
|
||||
val filterDownloaded = libraryPreferences.filterDownloaded().get()
|
||||
val filterUnread = libraryPreferences.filterUnread().get()
|
||||
val filterStarted = libraryPreferences.filterStarted().get()
|
||||
val filterBookmarked = libraryPreferences.filterBookmarked().get()
|
||||
val filterCompleted = libraryPreferences.filterCompleted().get()
|
||||
val loggedInServices = trackManager.services.filter { trackService -> trackService.isLogged }
|
||||
|
||||
val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged }
|
||||
.associate { trackService ->
|
||||
Pair(trackService.id, libraryPreferences.filterTracking(trackService.id.toInt()).get())
|
||||
trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
|
||||
}
|
||||
val isNotAnyLoggedIn = !loggedInServices.values.any()
|
||||
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
|
||||
|
||||
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null }
|
||||
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null }
|
||||
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
|
||||
|
||||
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
|
||||
if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
|
||||
@ -237,25 +223,21 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotAnyLoggedIn) return@tracking true
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val trackedManga = trackMap[item.libraryManga.manga.id]
|
||||
val mangaTracks = trackMap[item.libraryManga.id].orEmpty()
|
||||
|
||||
val containsExclude = loggedInServices.filterValues { it == State.EXCLUDE.value }
|
||||
val containsInclude = loggedInServices.filterValues { it == State.INCLUDE.value }
|
||||
val exclude = mangaTracks.filter { it in excludedTracks }
|
||||
val include = mangaTracks.filter { it in includedTracks }
|
||||
|
||||
if (!containsExclude.any() && !containsInclude.any()) return@tracking true
|
||||
|
||||
val exclude = trackedManga?.filterKeys { containsExclude.containsKey(it) }?.values ?: emptyList()
|
||||
val include = trackedManga?.filterKeys { containsInclude.containsKey(it) }?.values ?: emptyList()
|
||||
|
||||
if (containsInclude.any() && containsExclude.any()) {
|
||||
return@tracking if (exclude.isNotEmpty()) !exclude.any() else include.any()
|
||||
// TODO: Simplify the filter logic
|
||||
if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) {
|
||||
return@tracking if (exclude.isNotEmpty()) false else include.isNotEmpty()
|
||||
}
|
||||
|
||||
if (containsExclude.any()) return@tracking !exclude.any()
|
||||
if (excludedTracks.isNotEmpty()) return@tracking exclude.isEmpty()
|
||||
|
||||
if (containsInclude.any()) return@tracking include.any()
|
||||
if (includedTracks.isNotEmpty()) return@tracking include.isNotEmpty()
|
||||
|
||||
return@tracking false
|
||||
}
|
||||
@ -271,26 +253,28 @@ class LibraryPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
return this.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap {
|
||||
private fun LibraryMap.applySort(categories: List<Category>): LibraryMap {
|
||||
val sortModes = categories.associate { it.id to it.sort }
|
||||
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale))
|
||||
}
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
val sort = sortModes[i1.libraryManga.category]!!
|
||||
when (sort.type) {
|
||||
LibrarySort.Type.Alphabetical -> {
|
||||
collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale))
|
||||
sortAlphabetically(i1, i2)
|
||||
}
|
||||
LibrarySort.Type.LastRead -> {
|
||||
i1.libraryManga.lastRead.compareTo(i2.libraryManga.lastRead)
|
||||
@ -321,14 +305,14 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
return map.mapValues { entry ->
|
||||
return this.mapValues { entry ->
|
||||
val comparator = if (sortModes[entry.key]!!.isAscending) {
|
||||
Comparator(sortFn)
|
||||
} else {
|
||||
Collections.reverseOrder(sortFn)
|
||||
}
|
||||
|
||||
entry.value.sortedWith(comparator)
|
||||
entry.value.sortedWith(comparator.thenComparator(sortAlphabetically))
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,13 +326,18 @@ class LibraryPresenter(
|
||||
getLibraryManga.subscribe(),
|
||||
libraryPreferences.downloadBadge().changes(),
|
||||
libraryPreferences.filterDownloaded().changes(),
|
||||
preferences.downloadedOnly().changes(),
|
||||
downloadCache.changes,
|
||||
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, _ ->
|
||||
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ ->
|
||||
libraryMangaList
|
||||
.map { libraryManga ->
|
||||
val needsDownloadCounts = downloadBadgePref ||
|
||||
filterDownloadedPref != State.IGNORE.value ||
|
||||
downloadedOnly
|
||||
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
LibraryItem(libraryManga).apply {
|
||||
downloadCount = if (downloadBadgePref || filterDownloadedPref == State.INCLUDE.value) {
|
||||
downloadCount = if (needsDownloadCounts) {
|
||||
downloadManager.getDownloadCount(libraryManga.manga).toLong()
|
||||
} else {
|
||||
0
|
||||
@ -373,48 +362,11 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracked manga from the database and checks if the filter gets changed
|
||||
*
|
||||
* @return an observable of tracked manga.
|
||||
*/
|
||||
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
|
||||
return filterTriggerRelay.observeOn(Schedulers.io())
|
||||
.combineLatest(getTracksFlow().asObservable().observeOn(Schedulers.io())) { _, tracks -> tracks }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracked manga from the database
|
||||
*
|
||||
* @return an observable of tracked manga.
|
||||
*/
|
||||
private fun getTracksFlow(): Flow<Map<Long, Map<Long, Boolean>>> {
|
||||
// TODO: Move this to domain/data layer
|
||||
return getTracks.subscribe()
|
||||
.map { tracks ->
|
||||
tracks
|
||||
.groupBy { it.mangaId }
|
||||
.mapValues { tracksForMangaId ->
|
||||
// Check if any of the trackers is logged in for the current manga id
|
||||
tracksForMangaId.value.associate {
|
||||
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged ?: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
suspend fun requestFilterUpdate() = withIOContext {
|
||||
_filterChanges.send(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -432,9 +384,9 @@ class LibraryPresenter(
|
||||
*/
|
||||
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { getCategories.await(it.id) }
|
||||
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
|
||||
return mangas
|
||||
.map { getCategories.await(it.id).toSet() }
|
||||
.reduce { set1, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -444,9 +396,9 @@ class LibraryPresenter(
|
||||
*/
|
||||
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
val mangaCategories = mangas.toSet().map { getCategories.await(it.id) }
|
||||
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
|
||||
return mangaCategories.flatten().distinct().subtract(common).toMutableList()
|
||||
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
|
||||
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
|
||||
return mangaCategories.flatten().distinct().subtract(common)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -524,10 +476,10 @@ class LibraryPresenter(
|
||||
*/
|
||||
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
mangaList.map { manga ->
|
||||
mangaList.forEach { manga ->
|
||||
val categoryIds = getCategories.await(manga.id)
|
||||
.map { it.id }
|
||||
.subtract(removeCategories)
|
||||
.subtract(removeCategories.toSet())
|
||||
.plus(addCategories)
|
||||
.toList()
|
||||
|
||||
@ -649,10 +601,6 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
|
||||
return Observable.combineLatest(this, o2, combineFn)
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
|
||||
data class DeleteManga(val manga: List<Manga>) : Dialog()
|
||||
|
@ -48,7 +48,7 @@ class SetChapterSettingsDialog(bundle: Bundle? = null) : DialogController(bundle
|
||||
|
||||
activity?.toast(R.string.chapter_settings_updated)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ class SetTrackChaptersDialog<T> : DialogController
|
||||
np.clearFocus()
|
||||
listener.setChaptersRead(item, np.value)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ class SetTrackScoreDialog<T> : DialogController
|
||||
np.clearFocus()
|
||||
listener.setScore(item, np.value)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ class SetTrackStatusDialog<T> : DialogController
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
listener.setStatus(item, selectedIndex)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
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.preference.toggle
|
||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||
@ -275,6 +276,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_open_in_web_view -> {
|
||||
openChapterInWebview()
|
||||
}
|
||||
R.id.action_bookmark -> {
|
||||
presenter.bookmarkCurrentChapter(true)
|
||||
invalidateOptionsMenu()
|
||||
@ -665,6 +669,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
private fun openChapterInWebview() {
|
||||
val manga = presenter.manga ?: return
|
||||
val source = presenter.getSource() ?: return
|
||||
val url = presenter.getChapterUrl() ?: return
|
||||
|
||||
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun showReadingModeToast(mode: Int) {
|
||||
try {
|
||||
val strings = resources.getStringArray(R.array.viewers_selector)
|
||||
|
@ -40,7 +40,7 @@ class ReaderPageSheet(
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
activity.setAsCover(page)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
||||
@ -606,6 +607,15 @@ class ReaderPresenter(
|
||||
return viewerChaptersRelay.value?.currChapter
|
||||
}
|
||||
|
||||
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
|
||||
|
||||
fun getChapterUrl(): String? {
|
||||
val sChapter = getCurrentChapter()?.chapter ?: return null
|
||||
val source = getSource() ?: return null
|
||||
|
||||
return source.getChapterUrl(sChapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the currently active chapter.
|
||||
*/
|
||||
|
@ -27,8 +27,6 @@ import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
@ -202,7 +200,7 @@ object ImageUtil {
|
||||
/**
|
||||
* Splits tall images to improve performance of reader
|
||||
*/
|
||||
fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean {
|
||||
fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean {
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
|
||||
return true
|
||||
}
|
||||
@ -221,11 +219,15 @@ object ImageUtil {
|
||||
|
||||
return try {
|
||||
splitDataList.forEach { splitData ->
|
||||
val splitPath = splitImagePath(imageFilePath, splitData.index)
|
||||
val splitImageName = splitImageName(filenamePrefix, splitData.index)
|
||||
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
|
||||
tmpDir.findFile(splitImageName)?.delete()
|
||||
|
||||
val splitFile = tmpDir.createFile(splitImageName)
|
||||
|
||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||
|
||||
FileOutputStream(splitPath).use { outputStream ->
|
||||
splitFile.openOutputStream().use { outputStream ->
|
||||
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
|
||||
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
splitBitmap.recycle()
|
||||
@ -240,8 +242,8 @@ object ImageUtil {
|
||||
} catch (e: Exception) {
|
||||
// Image splits were not successfully saved so delete them and keep the original image
|
||||
splitDataList
|
||||
.map { splitImagePath(imageFilePath, it.index) }
|
||||
.forEach { File(it).delete() }
|
||||
.map { splitImageName(filenamePrefix, it.index) }
|
||||
.forEach { tmpDir.findFile(it)?.delete() }
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
} finally {
|
||||
@ -249,8 +251,7 @@ object ImageUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitImagePath(imageFilePath: String, index: Int) =
|
||||
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
|
||||
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg"
|
||||
|
||||
/**
|
||||
* Check whether the image is a long Strip that needs splitting
|
||||
|
7
app/src/main/res/drawable/ic_webview_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_webview_24dp.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<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="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user