Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 917f20894b | |||
| 3a3b719b8b | |||
| 1903437ecf | |||
| 5c26bb3a52 | |||
| 07599ade3a | |||
| 0a9f36402b | |||
| d2b325cd02 | |||
| cdc64aceb7 | |||
| 4bfd6e4026 | |||
| 50eebdf7d3 | |||
| f843de28d7 | |||
| d250a9a680 | |||
| 4130db3920 | |||
| f2cbff04ab | |||
| 061e9359e8 | |||
| 73258e9e05 | |||
| 73e4982ffb | |||
| 185cd923c0 | |||
| 3cfc53bf11 | |||
| 1301acfdb7 | |||
| 9d9dbea48d | |||
| c1df3eb1d0 | |||
| 3154c97aee | |||
| ffe1b160de | |||
| 23272375b7 | |||
| 863b6ee784 | |||
| c4c8d4b9c3 | |||
| b2bbbca585 | |||
| df3b879cf6 | |||
| 47c4f2cc8c | |||
| 905a1c1230 | |||
| bcaf7f6415 | |||
| 4639b3ecc3 | |||
| 2034971cc0 | |||
| bb8698b2a6 | |||
| cd69b09dd0 | |||
| 462b2164e8 | |||
| fb1a4ad828 | |||
| 3bd89cee26 | |||
| 6f43e98fff | |||
| 6feeb4b1ee | |||
| fcfe750fcf | |||
| 6e314e3643 | |||
| 487ca49c11 | |||
| 698abe8667 | |||
| 13c9daf9a9 | |||
| eb21454d6d | |||
| 56347e6d52 | |||
| 5c085a36e8 | |||
| 65ab676946 | |||
| 1f51569a35 | |||
| b0d6e16ca3 | |||
| 85cf54ccc8 | |||
| 602df5a729 | |||
| c8102836ce | |||
| e641575941 | |||
| 83afcee4d1 | |||
| 2102e0594e | |||
| 14c91da6b3 | |||
| 46c1c6463a | |||
| 89a521b836 | |||
| 65c6ed21ab | |||
| 1b911e7e15 | |||
| 0535e41051 | |||
| 3fc802f837 | |||
| 976b5cc03e | |||
| a9fe971337 | |||
| 5d1dbcb390 | |||
| 8d11ef3244 | |||
| 724a61f513 | |||
| 724c774dc9 | |||
| 29e0b2e4a5 | |||
| 2776e41127 | |||
| af1f77418f | |||
| c1df5da062 | |||
| f8f645772d | |||
| b1e6fa65d6 | |||
| 01e8c6cc12 |
@@ -100,5 +100,5 @@ body:
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
- label: I understand that **TachiyomiSY does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
required: true
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: ./gradlew spotlessCheck assembleDevDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: TachiyomiSY-${{ github.sha }}.apk
|
||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||
|
||||
@@ -31,7 +31,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
|
||||
versionCode = 75
|
||||
versionCode = 76
|
||||
versionName = "1.12.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
@@ -150,12 +150,14 @@ kotlin {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xannotation-default-target=param-property",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -265,6 +267,7 @@ dependencies {
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.bundles.markdown)
|
||||
implementation(libs.materialKolor)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
Vendored
+3
-1
@@ -298,4 +298,6 @@
|
||||
-dontwarn org.ietf.jgss.GSSException
|
||||
-dontwarn org.ietf.jgss.GSSManager
|
||||
-dontwarn org.ietf.jgss.GSSName
|
||||
-dontwarn org.ietf.jgss.Oid
|
||||
-dontwarn org.ietf.jgss.Oid
|
||||
-dontwarn com.google.re2j.Matcher
|
||||
-dontwarn com.google.re2j.Pattern
|
||||
|
||||
@@ -60,6 +60,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -156,6 +157,7 @@ class DomainModule : InjektModule {
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
|
||||
|
||||
+13
-12
@@ -30,8 +30,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.gowtham.ratingbar.RatingBar
|
||||
import com.gowtham.ratingbar.RatingBarConfig
|
||||
import com.gowtham.ratingbar.ComposeStars
|
||||
import com.gowtham.ratingbar.RatingBarStyle
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import exh.metadata.MetadataUtil
|
||||
@@ -222,17 +222,18 @@ fun BrowseSourceEHentaiListItem(
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
RatingBar(
|
||||
ComposeStars(
|
||||
value = rating,
|
||||
onValueChange = {},
|
||||
onRatingChanged = {},
|
||||
config = RatingBarConfig().apply {
|
||||
isIndicator(true)
|
||||
numStars(5)
|
||||
size(18.dp)
|
||||
activeColor(Color(0xFF005ED7))
|
||||
inactiveColor(Color(0xE1E2ECFF))
|
||||
},
|
||||
numOfStars = 5,
|
||||
size = 18.dp,
|
||||
spaceBetween = 2.dp,
|
||||
hideInactiveStars = false,
|
||||
style = RatingBarStyle.Fill(
|
||||
activeColor = Color(0xFF005ED7),
|
||||
inActiveColor = Color(0xE1E2ECFF),
|
||||
),
|
||||
painterEmpty = null,
|
||||
painterFilled = null,
|
||||
)
|
||||
val color = genre?.first?.color
|
||||
val res = genre?.second
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ package eu.kanade.presentation.browse.components
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
@@ -17,7 +17,7 @@ fun BrowseSourceFloatingActionButton(
|
||||
onFabClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||
|
||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||
onClick = onCreate,
|
||||
|
||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(it.title)
|
||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
||||
// Don't show search action
|
||||
} else if (searchQuery == null) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_search))
|
||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
||||
}
|
||||
} else if (searchQuery.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_reset))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun DownloadDropdownMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
offset: DpOffset? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (offset != null) {
|
||||
DropdownMenu(
|
||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
private fun DownloadDropdownMenuItems(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
) {
|
||||
@@ -59,6 +58,7 @@ private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||
)
|
||||
|
||||
options.map { (downloadAction, string) ->
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -27,9 +24,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -101,7 +100,6 @@ import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -167,7 +165,7 @@ fun MangaScreen(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -331,7 +329,7 @@ private fun MangaScreenSmallImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -418,25 +416,23 @@ private fun MangaScreenSmallImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
@@ -654,7 +650,7 @@ fun MangaScreenLargeImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -737,27 +733,25 @@ fun MangaScreenLargeImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
PullRefresh(
|
||||
@@ -953,7 +947,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||
// SY <--
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
) {
|
||||
items(
|
||||
@@ -1020,14 +1014,14 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
onChapterSelected(item, !item.selected, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
||||
NEXT_10_CHAPTERS,
|
||||
NEXT_25_CHAPTERS,
|
||||
UNREAD_CHAPTERS,
|
||||
BOOKMARKED_CHAPTERS,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
|
||||
@@ -93,10 +93,10 @@ fun MangaBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
@@ -260,10 +260,10 @@ fun LibraryBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
|
||||
@@ -605,44 +605,47 @@ private fun ColumnScope.MangaContentInfo(
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
@Composable
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||
markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
|
||||
@@ -245,7 +245,6 @@ object AboutScreen : Screen() {
|
||||
is GetApplicationRelease.Result.OsTooOld -> {
|
||||
context.toast(MR.strings.update_check_eol)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
+5
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||
LibrariesContainer(
|
||||
libraries = libraries,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ class DebugInfoScreen : Screen() {
|
||||
val status by produceState(initialValue = "-") {
|
||||
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
|
||||
value = when (result) {
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE_INSTALLED -> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
|
||||
"Compiled non-matching"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.presentation.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
||||
isAmoled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||
getThemeColorScheme(
|
||||
context = context,
|
||||
appTheme = appTheme,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getThemeColorScheme(
|
||||
context: Context,
|
||||
appTheme: AppTheme,
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
MonetColorScheme(context)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
||||
private val surfaceContainerHigh = Color(0xFF131313)
|
||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
fun getColorScheme(
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
overrideDarkSurfaceContainers: Boolean,
|
||||
): ColorScheme {
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
val amoledScheme = darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||
|
||||
return amoledScheme.copy(
|
||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||
surfaceContainerLowest = surfaceContainer,
|
||||
surfaceContainerLow = surfaceContainer,
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.UiModeManager
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
||||
import com.google.android.material.color.utilities.SchemeContent
|
||||
import com.google.android.material.color.utilities.Score
|
||||
import com.materialkolor.PaletteStyle
|
||||
import com.materialkolor.dynamiccolor.ColorSpec
|
||||
import com.materialkolor.ktx.DynamicScheme
|
||||
import com.materialkolor.toColorScheme
|
||||
|
||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
?.primaryColor
|
||||
?.toArgb()
|
||||
if (seed != null) {
|
||||
MonetCompatColorScheme(context, seed)
|
||||
MonetCompatColorScheme(Color(seed))
|
||||
} else {
|
||||
TachiyomiColorScheme
|
||||
}
|
||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme
|
||||
get() = monet.lightScheme
|
||||
|
||||
companion object {
|
||||
@Suppress("Unused")
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val bitmapPixels = IntArray(width * height)
|
||||
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
|
||||
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
|
||||
.takeIf { it != 0 } // Don't take fallback color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
||||
override val darkScheme = dynamicDarkColorScheme(context)
|
||||
}
|
||||
|
||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
||||
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||
|
||||
companion object {
|
||||
private fun Int.toComposeColor(): Color = Color(this)
|
||||
|
||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
||||
val scheme = SchemeContent(
|
||||
Hct.fromInt(seed),
|
||||
dark,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
)
|
||||
val dynamicColors = MaterialDynamicColors()
|
||||
return ColorScheme(
|
||||
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
|
||||
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
|
||||
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
|
||||
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
|
||||
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
|
||||
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
|
||||
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
|
||||
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
|
||||
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
|
||||
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
|
||||
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
|
||||
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
|
||||
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
|
||||
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
|
||||
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
|
||||
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
|
||||
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
|
||||
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
|
||||
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
|
||||
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
|
||||
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
|
||||
scrim = Color.Black,
|
||||
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
|
||||
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
|
||||
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||
return DynamicScheme(
|
||||
seedColor = seed,
|
||||
isDark = dark,
|
||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||
style = PaletteStyle.Expressive,
|
||||
)
|
||||
.toColorScheme(isAmoled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.platform.toClipEntry
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
@@ -73,6 +75,7 @@ import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
@@ -248,6 +251,7 @@ private fun SearchResultItem(
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
||||
expanded = dropDownMenuExpanded,
|
||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||
onCopyName = {
|
||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
||||
scope.launch {
|
||||
val clipEntry = ClipData.newPlainText(
|
||||
trackSearch.title,
|
||||
trackSearch.title,
|
||||
).toClipEntry()
|
||||
clipboard.setClipEntry(clipEntry)
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = {
|
||||
val url = trackSearch.tracking_url
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun UpdatesFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(screenModel = screenModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterSheet(
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = filterDownloaded,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
)
|
||||
|
||||
val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
)
|
||||
|
||||
val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
)
|
||||
|
||||
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
|
||||
|
||||
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState()
|
||||
|
||||
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { toggleScanlatorFilter() }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_filter_excluded_scanlators),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = filterExcludedScanlators,
|
||||
onCheckedChange = { toggleScanlatorFilter() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
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.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -37,6 +40,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -57,8 +61,10 @@ fun UpdateScreen(
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
onSelectAll(false)
|
||||
@@ -69,6 +75,8 @@ fun UpdateScreen(
|
||||
UpdatesAppBar(
|
||||
onCalendarClicked = { onCalendarClicked() },
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
onFilterClicked = { onFilterClicked() },
|
||||
hasFilters = hasActiveFilters,
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
@@ -139,6 +147,8 @@ fun UpdateScreen(
|
||||
private fun UpdatesAppBar(
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasFilters: Boolean,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
@@ -153,6 +163,12 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onFilterClicked,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_view_upcoming),
|
||||
icon = Icons.Outlined.CalendarMonth,
|
||||
|
||||
@@ -72,7 +72,7 @@ internal fun LazyListScope.updatesUiItems(
|
||||
// SY -->
|
||||
preserveReadingPosition: Boolean,
|
||||
// SY <--
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
@@ -120,11 +120,11 @@ internal fun LazyListScope.updatesUiItems(
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||
},
|
||||
onClick = {
|
||||
when {
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||
else -> onClickUpdate(updatesItem)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
import tachiyomi.i18n.MR
|
||||
import java.net.UnknownHostException
|
||||
|
||||
context(Context)
|
||||
context(context: Context)
|
||||
val Throwable.formattedMessage: String
|
||||
get() {
|
||||
when (this) {
|
||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
||||
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||
is UnknownHostException -> {
|
||||
return if (!isOnline()) {
|
||||
stringResource(MR.strings.exception_offline)
|
||||
return if (!context.isOnline()) {
|
||||
context.stringResource(MR.strings.exception_offline)
|
||||
} else {
|
||||
stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
context.stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
||||
is NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||
}
|
||||
return when (val className = this::class.simpleName) {
|
||||
"Exception", "IOException" -> message ?: className
|
||||
|
||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
// https://issuetracker.google.com/352584409
|
||||
context(LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
context(itemScope: LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ fun EhLoginWebViewScreen(
|
||||
)
|
||||
is LoadingState.Loading -> {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
(loadingState as? LoadingState.Loading)?.progress ?: 1f,
|
||||
loadingState.progress,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
label = "webview_loading",
|
||||
)
|
||||
|
||||
@@ -273,7 +273,7 @@ fun WebViewScreenContent(
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
is LoadingState.Loading -> LinearProgressIndicator(
|
||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
||||
progress = { loadingState.progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
|
||||
@@ -420,7 +420,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
is SourceNotInstalledException -> context.stringResource(
|
||||
MR.strings.loader_not_implemented_error,
|
||||
)
|
||||
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
@@ -692,8 +691,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return File("")
|
||||
}
|
||||
|
||||
@@ -737,10 +735,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
const val KEY_GROUP_EXTRA = "group_extra"
|
||||
// SY <--
|
||||
|
||||
fun cancelAllWorks(context: Context) {
|
||||
context.workManager.cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(
|
||||
context: Context,
|
||||
prefInterval: Int? = null,
|
||||
@@ -754,16 +748,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
val networkRequest = NetworkRequest.Builder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
@@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
@@ -34,7 +40,26 @@ class SyncYomiSyncService(
|
||||
|
||||
private class SyncYomiException(message: String?) : Exception(message)
|
||||
|
||||
@Serializable
|
||||
private data class SyncEvent(
|
||||
val event: SyncEventStatus,
|
||||
@SerialName("device_name")
|
||||
val deviceName: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private enum class SyncEventStatus {
|
||||
SYNC_STARTED,
|
||||
SYNC_SUCCESS,
|
||||
SYNC_FAILED,
|
||||
SYNC_ERROR,
|
||||
SYNC_CANCELLED,
|
||||
}
|
||||
|
||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||
|
||||
try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
@@ -52,11 +77,23 @@ class SyncYomiSyncService(
|
||||
syncData
|
||||
}
|
||||
|
||||
pushSyncData(finalSyncData, etag)
|
||||
val success = pushSyncData(finalSyncData, etag)
|
||||
|
||||
if (success) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||
} else {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||
}
|
||||
|
||||
return finalSyncData.backup
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||
throw e
|
||||
}
|
||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||
notifier.showSyncError(e.message)
|
||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -123,8 +160,8 @@ class SyncYomiSyncService(
|
||||
/**
|
||||
* Return true if update success
|
||||
*/
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||
val backup = syncData.backup ?: return
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
||||
val backup = syncData.backup ?: return true
|
||||
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
@@ -163,13 +200,49 @@ class SyncYomiSyncService(
|
||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.lastSyncEtag().set(newETag)
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||
return true
|
||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||
// other clients updated remote data, will try next time
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||
return false
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) {
|
||||
withContext(NonCancellable) {
|
||||
try {
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
val url = "$host/api/sync/event"
|
||||
|
||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
val bodyObj = SyncEvent(
|
||||
event = event,
|
||||
deviceName = android.os.Build.MODEL,
|
||||
message = message,
|
||||
)
|
||||
|
||||
val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val request = POST(
|
||||
url = url,
|
||||
headers = headers,
|
||||
body = requestBody,
|
||||
)
|
||||
|
||||
val client = OkHttpClient()
|
||||
client.newCall(request).await().close()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
||||
val query = $$"""
|
||||
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
@@ -82,14 +82,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
val query = $$"""
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|
||||
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|id: $listId, progress: $progress, status: $status, private: $private,
|
||||
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|
||||
|) {
|
||||
|id
|
||||
|status
|
||||
@@ -118,9 +118,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun deleteLibManga(track: DomainTrack) {
|
||||
withIOContext {
|
||||
val query = """
|
||||
|mutation DeleteManga(${'$'}listId: Int) {
|
||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||
val query = $$"""
|
||||
|mutation DeleteManga($listId: Int) {
|
||||
|DeleteMediaListEntry(id: $listId) {
|
||||
|deleted
|
||||
|}
|
||||
|}
|
||||
@@ -139,10 +139,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query Search(${'$'}query: String) {
|
||||
val query = $$"""
|
||||
|query Search($query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|id
|
||||
|staff {
|
||||
|edges {
|
||||
@@ -201,10 +201,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
val query = $$"""
|
||||
|query ($id: Int!, $manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|
||||
|id
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|
||||
@@ -113,7 +113,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
// Users can set a 'username' (not nickname) once which effectively
|
||||
// replaces the stringified ID in certain queries.
|
||||
// If no username is set, the API returns the user ID as a strings
|
||||
var username = api.getUsername()
|
||||
val username = api.getUsername()
|
||||
saveCredentials(username, oauth.accessToken)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
|
||||
@@ -137,7 +137,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
||||
}
|
||||
|
||||
authentication.apiUrl = prefApiUrl
|
||||
authentication.jwtToken = token.toString()
|
||||
authentication.jwtToken = token
|
||||
}
|
||||
authentications = oauth
|
||||
}
|
||||
|
||||
@@ -12,15 +12,12 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
@@ -80,15 +77,15 @@ class MyAnimeListApi(
|
||||
// MAL API throws a 400 when the query is over 64 characters...
|
||||
.appendQueryParameter("q", query.take(64))
|
||||
.appendQueryParameter("nsfw", "true")
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALSearchResult>()
|
||||
.data
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.filter { !it.publishing_type.contains("novel") }
|
||||
.filter { !(it.node.mediaType.contains("novel")) }
|
||||
.map { parseSearchItem(it.node) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,29 +94,13 @@ class MyAnimeListApi(
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendPath(id.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
||||
)
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALManga>()
|
||||
.let {
|
||||
TrackSearch.create(trackId).apply {
|
||||
remote_id = it.id
|
||||
title = it.title
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
score = it.mean
|
||||
cover_url = it.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
start_date = it.startDate ?: ""
|
||||
}
|
||||
}
|
||||
.let { parseSearchItem(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,8 +164,7 @@ class MyAnimeListApi(
|
||||
|
||||
val matches = myListSearchResult.data
|
||||
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.map { parseSearchItem(it.node) }
|
||||
|
||||
// Check next page if there's more
|
||||
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
||||
@@ -216,12 +196,12 @@ class MyAnimeListApi(
|
||||
description = it.synopsis,
|
||||
authors = it.authors
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.mapNotNull { it.node.getFullName() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
artists = it.authors
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.mapNotNull { it.node.getFullName() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
@@ -230,10 +210,10 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
||||
private suspend fun getListPage(offset: Int): MALSearchResult {
|
||||
return withIOContext {
|
||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
|
||||
if (offset > 0) {
|
||||
urlBuilder.appendQueryParameter("offset", offset.toString())
|
||||
@@ -262,6 +242,28 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
|
||||
return TrackSearch.create(trackId).apply {
|
||||
remote_id = searchItem.id
|
||||
title = searchItem.title
|
||||
summary = searchItem.synopsis
|
||||
total_chapters = searchItem.numChapters
|
||||
score = searchItem.mean
|
||||
cover_url = searchItem.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = searchItem.status.replace("_", " ")
|
||||
publishing_type = searchItem.mediaType.replace("_", " ")
|
||||
start_date = searchItem.startDate ?: ""
|
||||
artists = searchItem.authors
|
||||
.filter { authorNode -> authorNode.role == "Art" }
|
||||
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||
authors = searchItem.authors
|
||||
// count all with "Story" or "Story & Art" as authors, like is done for library entries
|
||||
.filter { authorNode -> authorNode.role.contains("Story") }
|
||||
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(isoDate: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
@@ -273,7 +275,7 @@ class MyAnimeListApi(
|
||||
return try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(epochTime)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -284,6 +286,9 @@ class MyAnimeListApi(
|
||||
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
|
||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
||||
|
||||
private const val SEARCH_FIELDS =
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date,authors{first_name,last_name}"
|
||||
|
||||
private const val LIST_PAGINATION_AMOUNT = 250
|
||||
|
||||
private var codeVerifier: String = ""
|
||||
|
||||
@@ -18,8 +18,26 @@ data class MALManga(
|
||||
val mediaType: String,
|
||||
@SerialName("start_date")
|
||||
val startDate: String?,
|
||||
val authors: List<MALAuthorNode> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
val node: MALAuthor,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val id: Int,
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
) {
|
||||
fun getFullName(): String? = "$firstName $lastName".trim().ifBlank { null }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
@@ -33,19 +51,5 @@ data class MALMangaMetadata(
|
||||
val synopsis: String?,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val authors: List<MALAuthor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val node: MALAuthorNode,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
val authors: List<MALAuthorNode>,
|
||||
)
|
||||
|
||||
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class MALSearchResult(
|
||||
val data: List<MALSearchResultNode>,
|
||||
val paging: MALSearchPaging,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultNode(
|
||||
val node: MALSearchResultItem,
|
||||
val node: MALManga,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultItem(
|
||||
val id: Int,
|
||||
data class MALSearchPaging(
|
||||
val next: String?,
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchResult(
|
||||
val data: List<MALUserSearchItem>,
|
||||
val paging: MALUserSearchPaging,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchItem(
|
||||
val node: MALUserSearchItemNode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchPaging(
|
||||
val next: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchItemNode(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
)
|
||||
@@ -37,14 +37,14 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
|
||||
|
||||
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||
val query = """
|
||||
|query GetManga(${'$'}mangaId: Int!) {
|
||||
| manga(id: ${'$'}mangaId) {
|
||||
val query = $$"""
|
||||
|query GetManga($mangaId: Int!) {
|
||||
| manga(id: $mangaId) {
|
||||
| ...MangaFragment
|
||||
| }
|
||||
|}
|
||||
|
|
||||
|$MangaFragment
|
||||
|$$MangaFragment
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
@@ -87,9 +87,9 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
|
||||
// TODO: Include a filter on the chapter number here
|
||||
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
|
||||
val chaptersQuery = """
|
||||
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
||||
val chaptersQuery = $$"""
|
||||
|query GetMangaUnreadChapters($mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
|
||||
| nodes {
|
||||
| id
|
||||
| chapterNumber
|
||||
@@ -115,24 +115,24 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.data
|
||||
.entry
|
||||
.nodes
|
||||
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
|
||||
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read + 0.001 } }
|
||||
}
|
||||
|
||||
val markQuery = if (deleteDownloadsOnServer) {
|
||||
"""
|
||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
| deleteDownloadedChapters(input: {ids: ${'$'}chapters}) {
|
||||
| deleteDownloadedChapters(input: {ids: $chapters}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
"""
|
||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
@@ -156,9 +156,9 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val trackQuery = """
|
||||
|mutation TrackManga(${'$'}mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
||||
val trackQuery = $$"""
|
||||
|mutation TrackManga($mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: $mangaId}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
|
||||
@@ -18,6 +18,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
|
||||
class PreferenceModule(val app: Application) : InjektModule {
|
||||
@@ -44,6 +45,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory {
|
||||
LibraryPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpdatesPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
ReaderPreferences(get())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
@SuppressLint("RequestInstallPackagesPolicy")
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
try {
|
||||
shellInterface?.install(
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
||||
)
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
|
||||
shellInterface?.install(it)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
continueQueue(InstallStep.Error)
|
||||
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun onDestroy() {
|
||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||
if (Shizuku.pingBinder()) {
|
||||
try {
|
||||
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) { "Failed to unbind shizuku service" }
|
||||
}
|
||||
}
|
||||
service.unregisterReceiver(receiver)
|
||||
logcat { "ShizukuInstaller destroy" }
|
||||
scope.cancel()
|
||||
|
||||
@@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
intent.data?.let { contentResolver.delete(it, null, null) }
|
||||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
|
||||
@@ -1,66 +1,48 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
*/
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
|
||||
/**
|
||||
* The broadcast receiver which listens to download completion events.
|
||||
*/
|
||||
private val downloadReceiver = DownloadCompletionReceiver()
|
||||
|
||||
/**
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||
internal class ExtensionInstaller(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val activeJobs = mutableMapOf<String, Job>()
|
||||
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
|
||||
private val httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
|
||||
val pkgName = extension.pkgName
|
||||
val downloadId = extension.pkgName.hashCode().toLong()
|
||||
cancelInstall(extension.pkgName)
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
val step = MutableStateFlow(InstallStep.Pending)
|
||||
activeSteps[downloadId] = step
|
||||
|
||||
// Register the receiver after removing (and unregistering) the previous download
|
||||
downloadReceiver.register()
|
||||
val job = scope.launch {
|
||||
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
|
||||
try {
|
||||
step.value = InstallStep.Downloading
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
val downloadUri = url.toUri()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Failed to download extension")
|
||||
}
|
||||
response.body.byteStream().use { input ->
|
||||
tmpFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
||||
downloadsStateFlows[id] = downloadStateFlow
|
||||
|
||||
// Poll download status
|
||||
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
|
||||
// Map to our model
|
||||
when (downloadStatus) {
|
||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||
else -> null
|
||||
step.value = InstallStep.Installing
|
||||
installApk(downloadId, tmpFile)
|
||||
} catch (e: Exception) {
|
||||
if (e is InterruptedException) {
|
||||
// Canceled
|
||||
} else {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
step.value = InstallStep.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
|
||||
emit(it)
|
||||
// Stop when the application is installed or errors
|
||||
!it.isCompleted()
|
||||
}.onCompletion {
|
||||
// Always notify on main thread
|
||||
withUIContext {
|
||||
// Always remove the download when unsubscribed
|
||||
deleteDownload(pkgName)
|
||||
activeJobs[extension.pkgName] = job
|
||||
|
||||
return step.asStateFlow()
|
||||
.onCompletion {
|
||||
activeJobs.remove(extension.pkgName)
|
||||
activeSteps.remove(downloadId)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow that polls the given download id for its status every second, as the
|
||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||
*
|
||||
* @param id The id of the download to poll.
|
||||
*/
|
||||
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
while (true) {
|
||||
// Get the current download status
|
||||
val downloadStatus = downloadManager.query(query).use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@flow
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||
}
|
||||
|
||||
emit(downloadStatus)
|
||||
|
||||
// Stop polling when the download fails or finishes
|
||||
if (
|
||||
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
||||
downloadStatus == DownloadManager.STATUS_FAILED
|
||||
) {
|
||||
return@flow
|
||||
}
|
||||
|
||||
delay(1.seconds)
|
||||
}
|
||||
}
|
||||
// Ignore duplicate results
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Starts an intent to install the extension at the given uri.
|
||||
*
|
||||
* @param uri The uri of the extension to install.
|
||||
* @param tempFile The file of the extension to install. Delete after use.
|
||||
*/
|
||||
fun installApk(downloadId: Long, uri: Uri) {
|
||||
private fun installApk(downloadId: Long, tempFile: File) {
|
||||
when (val installer = extensionInstaller.get()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
||||
|
||||
if (tempFile.exists() && !tempFile.delete()) {
|
||||
// Unlikely but just in case
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
||||
updateInstallStep(downloadId, InstallStep.Installed)
|
||||
} else {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
|
||||
tempFile.delete()
|
||||
}
|
||||
else -> {
|
||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
val intent = ExtensionInstallService.getIntent(
|
||||
context,
|
||||
downloadId,
|
||||
tempFile.getUriCompat(context),
|
||||
installer,
|
||||
)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -201,9 +140,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* Cancels extension install and remove from download manager and installer.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
Installer.cancelInstallQueue(context, downloadId)
|
||||
activeJobs.remove(pkgName)?.cancel()
|
||||
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param step New install step.
|
||||
*/
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
downloadsStateFlows[downloadId]?.let { it.value = step }
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the download for the given package name.
|
||||
*
|
||||
* @param pkgName The package name of the download to delete.
|
||||
*/
|
||||
private fun deleteDownload(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName)
|
||||
if (downloadId != null) {
|
||||
downloadManager.remove(downloadId)
|
||||
downloadsStateFlows.remove(downloadId)
|
||||
}
|
||||
if (activeDownloads.isEmpty()) {
|
||||
downloadReceiver.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver that listens to download status events.
|
||||
*/
|
||||
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Whether this receiver is currently registered.
|
||||
*/
|
||||
private var isRegistered = false
|
||||
|
||||
/**
|
||||
* Registers this receiver if it's not already.
|
||||
*/
|
||||
fun register() {
|
||||
if (isRegistered) return
|
||||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this receiver if it's not already.
|
||||
*/
|
||||
fun unregister() {
|
||||
if (!isRegistered) return
|
||||
isRegistered = false
|
||||
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download event is received. It looks for the download in the current active
|
||||
* downloads and notifies its installation step.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
|
||||
|
||||
// Avoid events for downloads we didn't request
|
||||
if (id !in activeDownloads.values) return
|
||||
|
||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
// Set next installation step
|
||||
if (uri == null) {
|
||||
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
||||
updateInstallStep(id, InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val localUri = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
||||
).removePrefix(FILE_SCHEME)
|
||||
|
||||
installApk(id, File(localUri).getUriCompat(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
activeSteps[downloadId]?.let { it.value = step }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
||||
|
||||
+60
-63
@@ -50,79 +50,46 @@ class ExtensionsScreenModel(
|
||||
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
val queryFilter: (String) -> ((Extension) -> Boolean) = { query ->
|
||||
filter@{ extension ->
|
||||
if (query.isEmpty()) return@filter true
|
||||
query.split(",").any { _input ->
|
||||
val input = _input.trim()
|
||||
if (input.isEmpty()) return@any false
|
||||
when (extension) {
|
||||
is Extension.Available -> {
|
||||
extension.sources.any {
|
||||
it.name.contains(input, ignoreCase = true) ||
|
||||
it.baseUrl.contains(input, ignoreCase = true) ||
|
||||
it.id == input.toLongOrNull()
|
||||
} ||
|
||||
extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
is Extension.Installed -> {
|
||||
extension.sources.any {
|
||||
it.name.contains(input, ignoreCase = true) ||
|
||||
it.id == input.toLongOrNull() ||
|
||||
if (it is HttpSource) {
|
||||
it.baseUrl.contains(input, ignoreCase = true)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} ||
|
||||
extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenModelScope.launchIO {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
||||
state.map { it.searchQuery }
|
||||
.distinctUntilChanged()
|
||||
.debounce(SEARCH_DEBOUNCE_MILLIS)
|
||||
.map { searchQueryPredicate(it ?: "") },
|
||||
currentDownloads,
|
||||
getExtensions.subscribe(),
|
||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
val searchQuery = query ?: ""
|
||||
|
||||
val itemsGroups: ItemGroups = mutableMapOf()
|
||||
|
||||
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates
|
||||
}
|
||||
|
||||
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = installed + untrusted
|
||||
}
|
||||
|
||||
val languagesWithExtensions = _available
|
||||
.filter(queryFilter(searchQuery))
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(LocaleHelper.comparator)
|
||||
.map { (lang, exts) ->
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||
exts.map(extensionMapper(downloads))
|
||||
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
buildMap {
|
||||
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||
}
|
||||
if (languagesWithExtensions.isNotEmpty()) {
|
||||
itemsGroups.putAll(languagesWithExtensions)
|
||||
}
|
||||
|
||||
itemsGroups
|
||||
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
|
||||
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
|
||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
|
||||
}
|
||||
|
||||
val languagesWithExtensions = _available
|
||||
.filter(predicate)
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(LocaleHelper.comparator)
|
||||
.map { (lang, exts) ->
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||
exts.map(extensionMapper(downloads))
|
||||
}
|
||||
if (languagesWithExtensions.isNotEmpty()) {
|
||||
putAll(languagesWithExtensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
.collectLatest {
|
||||
.collectLatest { items ->
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
items = it,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
|
||||
val subqueries = query.split(",")
|
||||
.map { it.trim() }
|
||||
.filterNot { it.isBlank() }
|
||||
|
||||
if (subqueries.isEmpty()) return { true }
|
||||
|
||||
return { extension ->
|
||||
subqueries.any { subquery ->
|
||||
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
|
||||
|
||||
when (extension) {
|
||||
is Extension.Installed -> extension.sources.any { source ->
|
||||
source.name.contains(subquery, ignoreCase = true) ||
|
||||
(source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true ||
|
||||
source.id == subquery.toLongOrNull()
|
||||
}
|
||||
|
||||
is Extension.Available -> extension.sources.any {
|
||||
it.name.contains(subquery, ignoreCase = true) ||
|
||||
it.baseUrl.contains(subquery, ignoreCase = true) ||
|
||||
it.id == subquery.toLongOrNull()
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
mutableState.update {
|
||||
it.copy(searchQuery = query)
|
||||
@@ -222,7 +219,7 @@ class ExtensionsScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
typealias ItemGroups = MutableMap<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
||||
typealias ItemGroups = Map<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
||||
|
||||
object ExtensionUiModel {
|
||||
sealed interface Header {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -49,6 +50,10 @@ fun extensionsTab(
|
||||
),
|
||||
),
|
||||
content = { contentPadding, _ ->
|
||||
BackHandler(enabled = state.searchQuery != null) {
|
||||
extensionsScreenModel.search(null)
|
||||
}
|
||||
|
||||
ExtensionScreen(
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
|
||||
+19
-15
@@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@@ -29,7 +32,6 @@ import mihon.feature.migration.config.MigrationConfigScreen
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (state.selectionMode) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.selectionMode,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
|
||||
+13
-9
@@ -1,17 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@@ -35,7 +38,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
@@ -74,13 +76,15 @@ data class MigrateSourceSearchScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.filters.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.download
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -16,8 +13,10 @@ import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Pill
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
)
|
||||
}
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (downloadList.isEmpty()) {
|
||||
|
||||
@@ -84,6 +84,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
@@ -122,6 +123,7 @@ class LibraryScreenModel(
|
||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
@@ -404,9 +406,7 @@ class LibraryScreenModel(
|
||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val mangaTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
||||
.orEmpty()
|
||||
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
||||
@@ -736,15 +736,19 @@ class LibraryScreenModel(
|
||||
* Queues the amount specified of unread chapters from the list of selected manga
|
||||
*/
|
||||
fun performDownloadAction(action: DownloadAction) {
|
||||
val mangas = state.value.selectedManga
|
||||
val amount = when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
||||
DownloadAction.UNREAD_CHAPTERS -> null
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
private fun downloadNextChapters(amount: Int?) {
|
||||
val mangas = state.value.selectedManga
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
// SY -->
|
||||
@@ -794,6 +798,54 @@ class LibraryScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadBookmarkedChapters() {
|
||||
val mangas = state.value.selectedManga
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
// SY -->
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
val mergedMangas = getMergedMangaById.await(manga.id)
|
||||
.associateBy { it.id }
|
||||
getBookmarkedChaptersByMangaId.await(manga.id)
|
||||
.groupBy { it.mangaId }
|
||||
.forEach ab@{ (mangaId, chapters) ->
|
||||
val mergedManga = mergedMangas[mangaId] ?: return@ab
|
||||
val downloadChapters = chapters.fastFilterNot { chapter ->
|
||||
downloadManager.queueState.value.fastAny { chapter.id == it.chapter.id } ||
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
mergedManga.ogTitle,
|
||||
mergedManga.source,
|
||||
)
|
||||
}
|
||||
|
||||
downloadManager.downloadChapters(mergedManga, downloadChapters)
|
||||
}
|
||||
|
||||
return@forEach
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val chapters = getBookmarkedChaptersByMangaId.await(manga.id)
|
||||
.fastFilterNot { chapter ->
|
||||
downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
// SY -->
|
||||
manga.ogTitle,
|
||||
// SY <--
|
||||
manga.source,
|
||||
)
|
||||
}
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun cleanTitles() {
|
||||
state.value.selectedManga.fastFilter {
|
||||
|
||||
@@ -177,7 +177,7 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@Suppress("KotlinConstantConditions")
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
val hasDebugOverlay = (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest")
|
||||
// SY <--
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
@@ -208,7 +207,9 @@ class MangaScreen(
|
||||
previewsRowCount = successState.previewsRowCount,
|
||||
onMigrateClicked = {
|
||||
navigator.push(MigrationConfigScreen(successState.manga.id))
|
||||
}.takeIf { successState.manga.favorite },
|
||||
}.takeIf {
|
||||
successState.manga.favorite /* SY --> */ && successState.manga.source != MERGED_SOURCE_ID /* SY <-- */
|
||||
},
|
||||
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||
// SY -->
|
||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||
@@ -403,12 +404,7 @@ class MangaScreen(
|
||||
try {
|
||||
getMangaUrl(manga_, source_)?.let { url ->
|
||||
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
@@ -1175,6 +1175,13 @@ class MangaScreenModel(
|
||||
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
|
||||
}
|
||||
|
||||
private fun getBookmarkedChapters(): List<Chapter> {
|
||||
val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty()
|
||||
return chapterItems
|
||||
.filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED }
|
||||
.map { it.chapter }
|
||||
}
|
||||
|
||||
private fun startDownload(
|
||||
chapters: List<Chapter>,
|
||||
startNow: Boolean,
|
||||
@@ -1237,6 +1244,7 @@ class MangaScreenModel(
|
||||
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
|
||||
}
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
startDownload(chaptersToDownload, false)
|
||||
@@ -1487,7 +1495,6 @@ class MangaScreenModel(
|
||||
fun toggleSelection(
|
||||
item: ChapterList.Item,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
updateSuccessState { successState ->
|
||||
@@ -1502,7 +1509,7 @@ class MangaScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.id, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -1528,7 +1535,7 @@ class MangaScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
|
||||
@@ -934,7 +934,7 @@ class ReaderActivity : BaseActivity() {
|
||||
private fun shareChapter() {
|
||||
assistUrl?.let {
|
||||
val intent = it.toUri().toShareIntent(this, type = "text/plain")
|
||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1139,7 @@ class ReaderActivity : BaseActivity() {
|
||||
context = applicationContext,
|
||||
message = /* SY --> */ text, // SY <--
|
||||
)
|
||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun onCopyImageResult(uri: Uri) {
|
||||
|
||||
@@ -655,7 +655,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
* if setting is enabled and [currentChapter] is queued for download
|
||||
*/
|
||||
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
|
||||
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also {
|
||||
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!)?.also {
|
||||
downloadManager.cancelQueuedDownloads(listOf(it))
|
||||
}
|
||||
}
|
||||
@@ -848,7 +848,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
viewModelScope.launchNonCancellable {
|
||||
updateChapter.await(
|
||||
ChapterUpdate(
|
||||
id = chapter.id!!.toLong(),
|
||||
id = chapter.id!!,
|
||||
bookmark = bookmarked,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -69,15 +69,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
||||
// selected as the current chapter when one of those pages is selected.
|
||||
val prevPages = chapters.prevChapter.pages
|
||||
if (prevPages != null) {
|
||||
newItems.addAll(prevPages.takeLast(2))
|
||||
}
|
||||
}
|
||||
// Add previous chapter pages and transition
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
@@ -119,14 +112,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// Add at most two pages, because this chapter will be selected before the user can
|
||||
// swap more pages.
|
||||
val nextPages = chapters.nextChapter.pages
|
||||
if (nextPages != null) {
|
||||
newItems.addAll(nextPages.take(2))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Resets double-page splits, else insert pages get misplaced
|
||||
subItems.filterIsInstance<InsertPage>().also { subItems.removeAll(it) }
|
||||
|
||||
@@ -43,14 +43,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
||||
// selected as the current chapter when one of those pages is selected.
|
||||
val prevPages = chapters.prevChapter.pages
|
||||
if (prevPages != null) {
|
||||
newItems.addAll(prevPages.takeLast(2))
|
||||
}
|
||||
}
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
@@ -70,14 +63,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// Add at most two pages, because this chapter will be selected before the user can
|
||||
// swap more pages.
|
||||
val nextPages = chapters.nextChapter.pages
|
||||
if (nextPages != null) {
|
||||
newItems.addAll(nextPages.take(2))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
updateItems(newItems)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.preference.asState
|
||||
@@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -43,9 +49,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.model.applyFilter
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
@@ -60,6 +68,7 @@ class UpdatesScreenModel(
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
// SY -->
|
||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
@@ -85,19 +94,35 @@ class UpdatesScreenModel(
|
||||
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
||||
|
||||
combine(
|
||||
getUpdates.subscribe(limit).distinctUntilChanged(),
|
||||
// needed for SQL filters (unread, started, bookmarked, etc)
|
||||
getUpdatesItemPreferenceFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
getUpdates.subscribe(
|
||||
limit,
|
||||
unread = it.filterUnread.toBooleanOrNull(),
|
||||
started = it.filterStarted.toBooleanOrNull(),
|
||||
bookmarked = it.filterBookmarked.toBooleanOrNull(),
|
||||
hideExcludedScanlators = it.filterExcludedScanlators,
|
||||
).distinctUntilChanged()
|
||||
},
|
||||
downloadCache.changes,
|
||||
downloadManager.queueState,
|
||||
) { updates, _, _ -> updates }
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
// needed for Kotlin filters (downloaded)
|
||||
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
|
||||
old.filterDownloaded == new.filterDownloaded
|
||||
},
|
||||
) { updates, _, _, itemPreferences ->
|
||||
updates
|
||||
.toUpdateItems()
|
||||
.applyFilters(itemPreferences)
|
||||
.toPersistentList()
|
||||
}
|
||||
.collectLatest { updateItems ->
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
items = updates.toUpdateItems(),
|
||||
items = updateItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,9 +133,43 @@ class UpdatesScreenModel(
|
||||
.catch { logcat(LogPriority.ERROR, it) }
|
||||
.collect(this@UpdatesScreenModel::updateDownloadState)
|
||||
}
|
||||
|
||||
getUpdatesItemPreferenceFlow()
|
||||
.map { prefs ->
|
||||
listOf(
|
||||
prefs.filterUnread,
|
||||
prefs.filterDownloaded,
|
||||
prefs.filterStarted,
|
||||
prefs.filterBookmarked,
|
||||
)
|
||||
.any { it != TriState.DISABLED }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
mutableState.update { state ->
|
||||
state.copy(hasActiveFilters = it)
|
||||
}
|
||||
}
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateItems(): PersistentList<UpdatesItem> {
|
||||
private fun List<UpdatesItem>.applyFilters(
|
||||
preferences: ItemPreferences,
|
||||
): List<UpdatesItem> {
|
||||
val filterDownloaded = preferences.filterDownloaded
|
||||
|
||||
val filterFnDownloaded: (UpdatesItem) -> Boolean = {
|
||||
applyFilter(filterDownloaded) {
|
||||
it.downloadStateProvider() == Download.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
||||
return fastFilter {
|
||||
filterFnDownloaded(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
||||
return this
|
||||
.map { update ->
|
||||
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
||||
@@ -135,7 +194,6 @@ class UpdatesScreenModel(
|
||||
selected = update.chapterId in selectedChapterIds,
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
fun updateLibrary(): Boolean {
|
||||
@@ -279,7 +337,6 @@ class UpdatesScreenModel(
|
||||
fun toggleSelection(
|
||||
item: UpdatesItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
mutableState.update { state ->
|
||||
@@ -294,7 +351,7 @@ class UpdatesScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -320,7 +377,7 @@ class UpdatesScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
@@ -373,9 +430,41 @@ class UpdatesScreenModel(
|
||||
libraryPreferences.newUpdatesCount().set(0)
|
||||
}
|
||||
|
||||
private fun getUpdatesItemPreferenceFlow(): Flow<ItemPreferences> {
|
||||
return combine(
|
||||
updatesPreferences.filterDownloaded().changes(),
|
||||
updatesPreferences.filterUnread().changes(),
|
||||
updatesPreferences.filterStarted().changes(),
|
||||
updatesPreferences.filterBookmarked().changes(),
|
||||
updatesPreferences.filterExcludedScanlators().changes(),
|
||||
) { downloaded, unread, started, bookmarked, excludedScanlators ->
|
||||
ItemPreferences(
|
||||
filterDownloaded = downloaded,
|
||||
filterUnread = unread,
|
||||
filterStarted = started,
|
||||
filterBookmarked = bookmarked,
|
||||
filterExcludedScanlators = excludedScanlators,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showFilterDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class ItemPreferences(
|
||||
val filterDownloaded: TriState,
|
||||
val filterUnread: TriState,
|
||||
val filterStarted: TriState,
|
||||
val filterBookmarked: TriState,
|
||||
val filterExcludedScanlators: Boolean,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
@@ -399,6 +488,7 @@ class UpdatesScreenModel(
|
||||
|
||||
sealed interface Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
||||
data object FilterSheet : Dialog
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
@@ -407,6 +497,14 @@ class UpdatesScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun TriState.toBooleanOrNull(): Boolean? {
|
||||
return when (this) {
|
||||
TriState.DISABLED -> null
|
||||
TriState.ENABLED_IS -> true
|
||||
TriState.ENABLED_NOT -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class UpdatesSettingsScreenModel(
|
||||
val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
) : ScreenModel {
|
||||
|
||||
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
|
||||
preference(updatesPreferences).getAndSet {
|
||||
it.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||
import eu.kanade.presentation.updates.UpdatesFilterDialog
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||
@@ -70,6 +71,7 @@ data object UpdatesTab : Tab {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
UpdateScreen(
|
||||
@@ -93,6 +95,8 @@ data object UpdatesTab : Tab {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||
onFilterClicked = screenModel::showFilterDialog,
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
)
|
||||
|
||||
val onDismissDialog = { screenModel.setDialog(null) }
|
||||
@@ -103,6 +107,12 @@ data object UpdatesTab : Tab {
|
||||
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||
)
|
||||
}
|
||||
is UpdatesScreenModel.Dialog.FilterSheet -> {
|
||||
UpdatesFilterDialog(
|
||||
onDismissRequest = onDismissDialog,
|
||||
screenModel = settingsScreenModel,
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class CrashLogUtil(
|
||||
|
||||
suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext {
|
||||
try {
|
||||
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
|
||||
val file = context.createFileInCacheDir("tachiyomi_sy_crash_logs.txt")
|
||||
|
||||
file.appendText(getDebugInfo() + "\n\n")
|
||||
getExtensionsInfo()?.let { file.appendText("$it\n\n") }
|
||||
|
||||
@@ -5,6 +5,7 @@ package androidx.preference
|
||||
/**
|
||||
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||
*/
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||
return onBindEditTextListener
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package exh.ui.login
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.CookieManager
|
||||
@@ -13,6 +12,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.presentation.webview.EhLoginWebViewScreen
|
||||
import eu.kanade.presentation.webview.components.IgneousDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -92,16 +92,32 @@ class EhLoginActivity : BaseActivity() {
|
||||
|
||||
private fun onPageFinished(view: WebView, url: String, customIgneous: String?) {
|
||||
xLogD(url)
|
||||
val parsedUrl = Uri.parse(url)
|
||||
val parsedUrl = url.toUri()
|
||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
}
|
||||
// Check login result
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
let html = document.documentElement.innerHTML;
|
||||
return html.includes("/cdn-cgi/");
|
||||
})();
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
val isCloudflareBlock = result == "true"
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
if (isCloudflareBlock) {
|
||||
xLogD("Cloudflare block detected — skipping logic")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
}
|
||||
// Check login result
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
}
|
||||
}
|
||||
} else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
|
||||
// At ExHentai, check that everything worked out...
|
||||
|
||||
@@ -73,6 +73,7 @@ class MigrateMangaUseCase(
|
||||
updatedChapter = updatedChapter.copy(
|
||||
dateFetch = prevChapter.dateFetch,
|
||||
bookmark = prevChapter.bookmark,
|
||||
lastPageRead = prevChapter.lastPageRead
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -62,7 +63,6 @@ import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.Pill
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -144,7 +144,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
||||
onClick = {
|
||||
@@ -331,13 +331,13 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
|
||||
private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) {
|
||||
mutableState.update { state ->
|
||||
val updatedSources = action(state.sources)
|
||||
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
||||
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
if (save) saveSources()
|
||||
saveSources()
|
||||
}
|
||||
|
||||
private fun initSources() {
|
||||
@@ -370,7 +370,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
}
|
||||
.toList()
|
||||
|
||||
updateSources(save = false) { sources }
|
||||
mutableState.update { state ->
|
||||
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelection(id: Long) {
|
||||
|
||||
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
|
||||
}
|
||||
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
State(
|
||||
current = current,
|
||||
target = target,
|
||||
applicableFlags = applicableFlags,
|
||||
|
||||
@@ -54,9 +54,11 @@ fun CalenderHeader(
|
||||
}
|
||||
Row {
|
||||
IconButton(onClick = onPreviousClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
||||
}
|
||||
IconButton(onClick = onNextClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.gradle.kotlin.dsl.the
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.File
|
||||
|
||||
@@ -42,7 +41,7 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *,
|
||||
compilerOptions {
|
||||
jvmTarget.set(AndroidConfig.JvmTarget)
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xcontext-receivers",
|
||||
"-Xcontext-parameters",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
|
||||
@@ -73,8 +72,6 @@ internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *,
|
||||
}
|
||||
|
||||
extensions.configure<ComposeCompilerGradlePluginExtension> {
|
||||
featureFlags.set(setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups))
|
||||
|
||||
val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean()
|
||||
val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").orNull.toBoolean()
|
||||
|
||||
|
||||
@@ -134,18 +134,18 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre
|
||||
return progressClient.newCall(request)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
context(_: Json)
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return decodeFromJsonResponse(serializer(), this)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
context(json: Json)
|
||||
fun <T> decodeFromJsonResponse(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
response: Response,
|
||||
): T {
|
||||
return response.body.source().use {
|
||||
decodeFromBufferedSource(deserializer, it)
|
||||
json.decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ class CloudflareInterceptor(
|
||||
executor.execute {
|
||||
webview = createWebView(originalRequest)
|
||||
|
||||
webview?.webViewClient = object : WebViewClientCompat() {
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
fun isCloudFlareBypassed(): Boolean {
|
||||
return cookieManager.get(origRequestUrl.toHttpUrl())
|
||||
@@ -111,7 +111,7 @@ class CloudflareInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
webview?.loadUrl(origRequestUrl, headers)
|
||||
webview.loadUrl(origRequestUrl, headers)
|
||||
}
|
||||
|
||||
latch.awaitFor30Seconds()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
abstract class WebViewClientCompat : WebViewClient() {
|
||||
@@ -28,7 +28,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
||||
) {
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
final override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
@@ -36,6 +36,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
||||
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
@Deprecated("shouldOverrideUrlLoading(WebView, WebResourceRequest)")
|
||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrlCompat(view, url)
|
||||
}
|
||||
@@ -47,6 +48,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
@Deprecated("shouldInterceptRequest(WebView, WebResourceRequest)")
|
||||
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, url)
|
||||
}
|
||||
@@ -65,6 +67,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("onReceivedError(WebView, WebResourceRequest, WebResourceError)")
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
|
||||
@@ -7,6 +7,7 @@ import me.zhanghai.android.libarchive.ArchiveException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.concurrent.Volatile
|
||||
import mihon.core.common.archive.ArchiveEntry as MihonArchiveEntry
|
||||
|
||||
class ArchiveInputStream(
|
||||
buffer: Long,
|
||||
@@ -67,18 +68,20 @@ class ArchiveInputStream(
|
||||
Archive.readFree(archive)
|
||||
}
|
||||
|
||||
fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry ->
|
||||
val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null
|
||||
val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG
|
||||
// SY -->
|
||||
val isEncrypted = ArchiveEntry.isEncrypted(entry)
|
||||
// SY <--
|
||||
ArchiveEntry(
|
||||
name,
|
||||
isFile,
|
||||
fun getNextEntry(): MihonArchiveEntry? {
|
||||
return Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry ->
|
||||
val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null
|
||||
val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG
|
||||
// SY -->
|
||||
isEncrypted,
|
||||
val isEncrypted = ArchiveEntry.isEncrypted(entry)
|
||||
// SY <--
|
||||
)
|
||||
MihonArchiveEntry(
|
||||
name,
|
||||
isFile,
|
||||
// SY -->
|
||||
isEncrypted,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,21 @@ class AndroidDatabaseHandler(
|
||||
// SY -->
|
||||
fun getLibraryQuery(condition: String = "M.favorite = 1") = LibraryQuery(driver, condition)
|
||||
|
||||
fun getUpdatesQuery(after: Long, limit: Long) = UpdatesQuery(driver, after, limit)
|
||||
fun getUpdatesQuery(
|
||||
after: Long,
|
||||
limit: Long,
|
||||
read: Boolean?,
|
||||
started: Long?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Long,
|
||||
) = UpdatesQuery(
|
||||
driver,
|
||||
after,
|
||||
limit,
|
||||
read,
|
||||
started,
|
||||
bookmarked,
|
||||
hideExcludedScanlators,
|
||||
)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -24,72 +24,113 @@ private val mapper = { cursor: SqlCursor ->
|
||||
cursor.getLong(12)!!,
|
||||
cursor.getLong(13)!!,
|
||||
cursor.getLong(14)!!,
|
||||
cursor.getString(15),
|
||||
)
|
||||
}
|
||||
|
||||
class UpdatesQuery(val driver: SqlDriver, val after: Long, val limit: Long) : ExecutableQuery<UpdatesView>(mapper) {
|
||||
class UpdatesQuery(
|
||||
val driver: SqlDriver,
|
||||
val after: Long,
|
||||
val limit: Long,
|
||||
val read: Boolean?,
|
||||
val started: Long?,
|
||||
val bookmarked: Boolean?,
|
||||
val hideExcludedScanlators: Long,
|
||||
) : ExecutableQuery<UpdatesView>(mapper) {
|
||||
override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> {
|
||||
return driver.executeQuery(
|
||||
null,
|
||||
"""
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
WHERE favorite = 1 AND source <> $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
AND dateUpload > :after
|
||||
UNION
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch
|
||||
FROM mangas
|
||||
LEFT JOIN (
|
||||
SELECT merged.manga_id,merged.merge_id
|
||||
FROM merged
|
||||
GROUP BY merged.merge_id
|
||||
) as ME
|
||||
ON ME.merge_id = mangas._id
|
||||
JOIN chapters
|
||||
ON ME.manga_id = chapters.manga_id
|
||||
WHERE favorite = 1 AND source = $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
AND dateUpload > :after
|
||||
ORDER BY datefetch DESC
|
||||
SELECT *
|
||||
FROM (
|
||||
-- Normal source
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch,
|
||||
excluded_scanlators.scanlator AS excludedScanlator
|
||||
FROM mangas
|
||||
JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON mangas._id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
WHERE mangas.source <> $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Merged source
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch,
|
||||
excluded_scanlators.scanlator AS excludedScanlator
|
||||
FROM mangas
|
||||
LEFT JOIN (
|
||||
SELECT merged.manga_id, merged.merge_id
|
||||
FROM merged
|
||||
GROUP BY merged.merge_id
|
||||
) AS ME
|
||||
ON ME.merge_id = mangas._id
|
||||
JOIN chapters
|
||||
ON ME.manga_id = chapters.manga_id
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON ME.merge_id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
WHERE mangas.source = $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
) AS combined
|
||||
WHERE
|
||||
favorite = 1
|
||||
AND dateUpload > :after
|
||||
AND (:read IS NULL OR read = :read)
|
||||
AND (
|
||||
:started IS NULL
|
||||
OR (:started = 1 AND last_page_read > 0 AND read = 0)
|
||||
OR (:started = 0 AND last_page_read = 0 AND read = 0)
|
||||
)
|
||||
AND (:bookmarked IS NULL OR bookmark = :bookmarked)
|
||||
AND (
|
||||
excludedScanlator IS NULL OR :hideExcludedScanlators = 0
|
||||
)
|
||||
ORDER BY datefetch DESC
|
||||
LIMIT :limit;
|
||||
""".trimIndent(),
|
||||
mapper,
|
||||
2,
|
||||
6,
|
||||
binders = {
|
||||
bindLong(0, after)
|
||||
bindLong(1, limit)
|
||||
var parameterIndex = 0
|
||||
bindLong(parameterIndex++, after)
|
||||
bindBoolean(parameterIndex++, read)
|
||||
bindLong(parameterIndex++, started)
|
||||
bindBoolean(parameterIndex++, bookmarked)
|
||||
bindLong(parameterIndex++, hideExcludedScanlators)
|
||||
bindLong(parameterIndex++, limit)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package tachiyomi.data.updates
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.core.common.util.lang.toLong
|
||||
import tachiyomi.data.AndroidDatabaseHandler
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -28,12 +29,36 @@ class UpdatesRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun subscribeAll(after: Long, limit: Long): Flow<List<UpdatesWithRelations>> {
|
||||
override fun subscribeAll(
|
||||
after: Long,
|
||||
limit: Long,
|
||||
unread: Boolean?,
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return databaseHandler.subscribeToList {
|
||||
updatesViewQueries.getRecentUpdates(after, limit, ::mapUpdatesWithRelations)
|
||||
updatesViewQueries.getRecentUpdatesWithFilters(
|
||||
after = after,
|
||||
limit = limit,
|
||||
// invert because unread in Kotlin -> read column in SQL
|
||||
read = unread?.let { !it },
|
||||
started = started?.toLong(),
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators.toLong(),
|
||||
mapper = ::mapUpdatesWithRelations,
|
||||
)
|
||||
}.map {
|
||||
databaseHandler.awaitListExecutable {
|
||||
(databaseHandler as AndroidDatabaseHandler).getUpdatesQuery(after, limit)
|
||||
(databaseHandler as AndroidDatabaseHandler).getUpdatesQuery(
|
||||
after = after,
|
||||
limit = limit,
|
||||
// invert because unread in Kotlin -> read column in SQL
|
||||
read = unread?.let { !it },
|
||||
started = started?.toLong(),
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators.toLong(),
|
||||
)
|
||||
}
|
||||
.map(::mapUpdatesView)
|
||||
}
|
||||
@@ -70,6 +95,7 @@ class UpdatesRepositoryImpl(
|
||||
coverLastModified: Long,
|
||||
dateUpload: Long,
|
||||
dateFetch: Long,
|
||||
excludedScanlator: String?,
|
||||
): UpdatesWithRelations = UpdatesWithRelations(
|
||||
mangaId = mangaId,
|
||||
// SY -->
|
||||
|
||||
@@ -22,6 +22,7 @@ CREATE TABLE chapters(
|
||||
|
||||
CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
|
||||
CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0;
|
||||
CREATE INDEX idx_chapters_url ON chapters(url);
|
||||
|
||||
CREATE TRIGGER update_last_modified_at_chapters
|
||||
AFTER UPDATE ON chapters
|
||||
|
||||
@@ -6,6 +6,7 @@ CREATE TABLE excluded_scanlators(
|
||||
);
|
||||
|
||||
CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id);
|
||||
CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator);
|
||||
|
||||
insert:
|
||||
INSERT INTO excluded_scanlators(manga_id, scanlator)
|
||||
|
||||
@@ -10,6 +10,7 @@ CREATE TABLE history(
|
||||
);
|
||||
|
||||
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
|
||||
CREATE INDEX idx_history_last_read ON history(last_read);
|
||||
|
||||
getHistoryByMangaId:
|
||||
SELECT
|
||||
|
||||
@@ -20,6 +20,8 @@ CREATE TABLE manga_sync(
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id);
|
||||
|
||||
delete:
|
||||
DELETE FROM manga_sync
|
||||
WHERE manga_id = :mangaId AND sync_id = :syncId;
|
||||
|
||||
@@ -34,6 +34,7 @@ CREATE TABLE mangas(
|
||||
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
CREATE INDEX mangas_url_index ON mangas(url);
|
||||
CREATE INDEX idx_mangas_source ON mangas(source);
|
||||
|
||||
CREATE TRIGGER update_last_favorited_at_mangas
|
||||
AFTER UPDATE OF favorite ON mangas
|
||||
@@ -118,12 +119,23 @@ AND source = :sourceId;
|
||||
|
||||
getDuplicateLibraryManga:
|
||||
WITH
|
||||
track_dupes AS (
|
||||
SELECT S2.manga_id
|
||||
FROM manga_sync S1
|
||||
INNER JOIN manga_sync S2
|
||||
ON S1.sync_id = S2.sync_id
|
||||
AND S1.remote_id = S2.remote_id
|
||||
AND S1.manga_id != S2.manga_id
|
||||
WHERE S1.manga_id = :id
|
||||
),
|
||||
duplicates AS (
|
||||
SELECT *
|
||||
FROM mangas
|
||||
SELECT M.*
|
||||
FROM mangas M
|
||||
LEFT JOIN track_dupes D
|
||||
ON D.manga_id = _id
|
||||
WHERE favorite = 1
|
||||
AND _id != :id
|
||||
AND lower(title) LIKE '%' || lower(:title) || '%'
|
||||
AND (lower(title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL)
|
||||
),
|
||||
chapter_counts AS (
|
||||
SELECT
|
||||
|
||||
@@ -8,6 +8,9 @@ CREATE TABLE mangas_categories(
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mangas_categories_manga_id ON mangas_categories(manga_id);
|
||||
CREATE INDEX idx_mangas_categories_category_id ON mangas_categories(category_id);
|
||||
|
||||
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||
BEGIN
|
||||
UPDATE mangas
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Add excluded_scanlators to updatesView
|
||||
DROP VIEW IF EXISTS updatesView;
|
||||
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch,
|
||||
excluded_scanlators.scanlator AS excludedScanlator
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON mangas._id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
WHERE favorite = 1
|
||||
AND date_fetch > date_added
|
||||
ORDER BY date_fetch DESC;
|
||||
|
||||
-- Migration to add performance indexes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mangas_source ON mangas(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_chapters_url ON chapters(url);
|
||||
CREATE INDEX IF NOT EXISTS idx_mangas_categories_manga_id ON mangas_categories(manga_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mangas_categories_category_id ON mangas_categories(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator);
|
||||
CREATE INDEX IF NOT EXISTS idx_manga_sync_manga_id ON manga_sync(manga_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_last_read ON history(last_read);
|
||||
@@ -14,9 +14,13 @@ SELECT
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch
|
||||
chapters.date_fetch AS datefetch,
|
||||
excluded_scanlators.scanlator AS excludedScanlator
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON mangas._id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
WHERE favorite = 1
|
||||
AND date_fetch > date_added
|
||||
ORDER BY date_fetch DESC;
|
||||
@@ -27,6 +31,23 @@ FROM updatesView
|
||||
WHERE dateUpload > :after
|
||||
LIMIT :limit;
|
||||
|
||||
getRecentUpdatesWithFilters:
|
||||
SELECT *
|
||||
FROM updatesView
|
||||
WHERE dateUpload > :after
|
||||
AND (:read IS NULL OR read = :read)
|
||||
-- Started means some progress but not finished, Read means finished chapter, thus:
|
||||
AND (
|
||||
:started IS NULL
|
||||
OR (:started = 1 AND last_page_read > 0 AND read = 0)
|
||||
OR (:started = 0 AND last_page_read = 0 AND read = 0)
|
||||
)
|
||||
AND (:bookmarked IS NULL OR bookmark = :bookmarked)
|
||||
AND (
|
||||
(excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
|
||||
)
|
||||
LIMIT :limit;
|
||||
|
||||
getUpdatesByReadStatus:
|
||||
SELECT *
|
||||
FROM updatesView
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package tachiyomi.domain.chapter.interactor
|
||||
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
|
||||
class GetBookmarkedChaptersByMangaId(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
// SY -->
|
||||
private val getManga: GetManga,
|
||||
private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId,
|
||||
// SY <--
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): List<Chapter> {
|
||||
return try {
|
||||
// SY -->
|
||||
val manga = getManga.await(mangaId) ?: return emptyList()
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
return getMergedChaptersByMangaId.await(mangaId, applyScanlatorFilter = true)
|
||||
.filter { it.bookmark }
|
||||
}
|
||||
// SY <--
|
||||
chapterRepository.getBookmarkedChaptersByMangaId(mangaId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,21 @@ class GetUpdates(
|
||||
// SY <--
|
||||
}
|
||||
|
||||
fun subscribe(instant: Instant): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(instant.toEpochMilli(), limit = 500)
|
||||
fun subscribe(
|
||||
instant: Instant,
|
||||
unread: Boolean?,
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(
|
||||
instant.toEpochMilli(),
|
||||
limit = 500,
|
||||
unread = unread,
|
||||
started = started,
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators,
|
||||
)
|
||||
// SY -->
|
||||
.catchNPE()
|
||||
// SY <--
|
||||
|
||||
@@ -7,7 +7,14 @@ interface UpdatesRepository {
|
||||
|
||||
suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations>
|
||||
|
||||
fun subscribeAll(after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
fun subscribeAll(
|
||||
after: Long,
|
||||
limit: Long,
|
||||
unread: Boolean?,
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package tachiyomi.domain.updates.service
|
||||
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
|
||||
class UpdatesPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun filterDownloaded() = preferenceStore.getEnum(
|
||||
"pref_filter_updates_downloaded",
|
||||
TriState.DISABLED,
|
||||
)
|
||||
|
||||
fun filterUnread() = preferenceStore.getEnum(
|
||||
"pref_filter_updates_unread",
|
||||
TriState.DISABLED,
|
||||
)
|
||||
|
||||
fun filterStarted() = preferenceStore.getEnum(
|
||||
"pref_filter_updates_started",
|
||||
TriState.DISABLED,
|
||||
)
|
||||
|
||||
fun filterBookmarked() = preferenceStore.getEnum(
|
||||
"pref_filter_updates_bookmarked",
|
||||
TriState.DISABLED,
|
||||
)
|
||||
|
||||
fun filterExcludedScanlators() = preferenceStore.getBoolean(
|
||||
"pref_filter_updates_hide_excluded_scanlators",
|
||||
false,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
agp_version = "8.13.2"
|
||||
lifecycle_version = "2.10.0"
|
||||
paging_version = "3.3.6"
|
||||
paging_version = "3.4.1"
|
||||
interpolator_version = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
@@ -21,7 +21,7 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref
|
||||
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }
|
||||
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
|
||||
|
||||
workmanager = "androidx.work:work-runtime:2.11.0"
|
||||
workmanager = "androidx.work:work-runtime:2.11.1"
|
||||
|
||||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
compose-bom = "2025.12.01"
|
||||
compose-bom = "2026.02.00"
|
||||
|
||||
[libraries]
|
||||
activity = "androidx.activity:activity-compose:1.12.2"
|
||||
activity = "androidx.activity:activity-compose:1.12.4"
|
||||
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
animation = { module = "androidx.compose.animation:animation" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
kotlin_version = "2.3.0"
|
||||
serialization_version = "1.9.0"
|
||||
kotlin_version = "2.3.10"
|
||||
serialization_version = "1.10.0"
|
||||
xml_serialization_version = "0.91.3"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
[versions]
|
||||
aboutlib_version = "13.2.1"
|
||||
leakcanary = "2.14"
|
||||
moko = "0.25.2"
|
||||
moko = "0.26.0"
|
||||
okhttp_version = "5.3.2"
|
||||
shizuku_version = "13.1.5"
|
||||
sqldelight = "2.2.1"
|
||||
sqlite = "2.6.2"
|
||||
voyager = "1.1.0-beta03"
|
||||
spotless = "8.1.0"
|
||||
spotless = "8.2.1"
|
||||
ktlint-core = "1.8.0"
|
||||
firebase-bom = "34.7.0"
|
||||
markdown = "0.39.0"
|
||||
junit = "6.0.1"
|
||||
firebase-bom = "34.9.0"
|
||||
markdown = "0.39.2"
|
||||
junit = "6.0.3"
|
||||
materialKolor = "5.0.0-alpha06"
|
||||
|
||||
[libraries]
|
||||
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
|
||||
@@ -29,7 +30,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3"
|
||||
|
||||
quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" }
|
||||
|
||||
jsoup = "org.jsoup:jsoup:1.21.2"
|
||||
jsoup = "org.jsoup:jsoup:1.22.1"
|
||||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc"
|
||||
@@ -43,7 +44,7 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1"
|
||||
|
||||
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
|
||||
|
||||
coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.3.0" }
|
||||
coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.4.0" }
|
||||
coil-core = { module = "io.coil-kt.coil3:coil" }
|
||||
coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
|
||||
@@ -90,8 +91,8 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect",
|
||||
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:6.0.7"
|
||||
mockk = "io.mockk:mockk:1.14.7"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:6.1.4"
|
||||
mockk = "io.mockk:mockk:1.14.9"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
|
||||
@@ -106,6 +107,8 @@ markdown-coil = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3",
|
||||
|
||||
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version = "0.1.0" }
|
||||
|
||||
materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
|
||||
|
||||
[plugins]
|
||||
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
||||
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" }
|
||||
|
||||
@@ -5,11 +5,11 @@ koin = "4.1.1"
|
||||
xlog = "com.elvishew:xlog:1.11.1"
|
||||
|
||||
ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0"
|
||||
composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.2.3"
|
||||
composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.3.12"
|
||||
|
||||
versionsx = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
||||
|
||||
sqlcipher = "net.zetetic:sqlcipher-android:4.12.0"
|
||||
sqlcipher = "net.zetetic:sqlcipher-android:4.13.0"
|
||||
|
||||
exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -91,10 +91,10 @@
|
||||
<plurals name="num_lock_times">
|
||||
<item quantity="zero">وقت الإنغلاق</item>
|
||||
<item quantity="one">وقت الإغلاق</item>
|
||||
<item quantity="two"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="many"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="two">اوقات الإغلاق</item>
|
||||
<item quantity="few">اوقات الإغلاق</item>
|
||||
<item quantity="many">اوقات الإغلاق</item>
|
||||
<item quantity="other">اوقات الإغلاق</item>
|
||||
</plurals>
|
||||
<plurals name="migrate_entry">
|
||||
<item quantity="zero">ترحيل المدخل؟</item>
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<string name="use_hentai_at_home">استخدم شبكة Hentai@Home</string>
|
||||
<string name="show_japanese_titles_option_2">يتم حاليًا عرض العناوين الإنجليزية/الحروف اللاتينية في نتائج البحث. امسح ذاكرة التخزين المؤقت للفصل بعد تغيير هذا (في القسم المتقدم)</string>
|
||||
<string name="watched_tags_exh">فئاتExHentai التي شاهدتها</string>
|
||||
<string name="tag_filtering_threshhold_error">يجب ان تكون القيمة بين صفر و ٩٩٩٩‐</string>
|
||||
<string name="tag_filtering_threshhold_error">يجب ان تكون القيمة بين صفر و ٩٩٩٩‐!</string>
|
||||
<string name="tag_watching_threshhold_summary">سيتم تضمين المعارض التي تم تحميلها مؤخرًا على شاشة المشاهدة إذا كانت تحتوي على علامة مشاهدة واحدة على الأقل ذات وزن إيجابي، ويصل مجموع الأوزان على العلامات التي تمت مشاهدتها إلى هذه القيمة أو أعلى. يمكن تعيين هذه العتبة بين ٠ و٩٩٩٩. حاليًا القيمة :%1$d</string>
|
||||
<string name="tag_watching_threshhold_error">يجب ان تكون القيمة بين صفر و ٩٩٩٩ !</string>
|
||||
<string name="language_filtering_summary">إذا كنت ترغب في إخفاء المعارض التى بلغات معينة من قائمة المعرض والبحث، اختارها في الحوار الذى سوف يظهر.
|
||||
@@ -245,16 +245,7 @@
|
||||
<string name="show_updater_statistics">عرض إحصائيات التحديث</string>
|
||||
<string name="gallery_updater_statistics_collection">جارٍ جمع الإحصائيات…</string>
|
||||
<string name="gallery_updater_statistics">إحصائيات تحديث المعرض</string>
|
||||
<string name="gallery_updater_stats_time">"
|
||||
\nالمعارض التي تم فحصها في الماضي
|
||||
\n- ساعة : %1$d
|
||||
\n-٦ ساعات : %2$d
|
||||
\n-١٢ ساعة : %3$d
|
||||
\n-يوم : %4$d
|
||||
\n-يومين: %5$d
|
||||
\n-أسبوع : %6$d
|
||||
\n-شهر : %7$d
|
||||
\n-سنة : %8$d"</string>
|
||||
<string name="gallery_updater_stats_time">\nالمعارض التي تم فحصها في الماضي\n\n- ساعة : %1$d\n\n-٦ ساعات : %2$d\n\n-١٢ ساعة : %3$d\n\n-يوم : %4$d\n\n-يومين: %5$d\n\n-أسبوع : %6$d\n\n-شهر : %7$d\n\n-سنة : %8$d</string>
|
||||
<string name="skip_page_restyling">تخطي إعادة تصميم الصفحة</string>
|
||||
<string name="eh_settings_uploading_to_server">تحميل الإعدادات إلى الخادم</string>
|
||||
<string name="eh_settings_configuration_failed_message">حدث خطأ أثناء عملية التكوين:%1$s</string>
|
||||
@@ -538,7 +529,7 @@
|
||||
<string name="favorites_sync_notes">ملاحظات مزامنة المفضلة الهامة</string>
|
||||
<string name="eh_batch_add_finish">إنهاء</string>
|
||||
<string name="favorites_sync_failed_to_add_to_local_error">\'%1$s\'%2$s</string>
|
||||
<string name="rating9">رائع</string>
|
||||
<string name="rating9">مدهش</string>
|
||||
<string name="relation_alternate_story">قصة بديلة</string>
|
||||
<string name="similar_titles">عناوين متشابهة</string>
|
||||
<string name="mangadex_preffered_source">مصدر MangaDex المفضل</string>
|
||||
@@ -575,7 +566,7 @@
|
||||
<string name="page_preview_page_go_to">ادخل إلى</string>
|
||||
<string name="rating2">مؤلم</string>
|
||||
<string name="no_rating">لا تقييم</string>
|
||||
<string name="artist_cg">الفنان</string>
|
||||
<string name="artist_cg">فنان رسومات حاسوبية</string>
|
||||
<string name="genre">النوع</string>
|
||||
<string name="merged_toggle_download_chapters_error">خطأ في تنزيل الفصول</string>
|
||||
<string name="merged_references_invalid">اندماج المراجع غير صالح</string>
|
||||
@@ -636,10 +627,15 @@
|
||||
<string name="select_scanlators">مجموعات المسح للعرض</string>
|
||||
<string name="relation_spin_off">تدور خارج</string>
|
||||
<string name="doujinshi">دوجينشي</string>
|
||||
<string name="non_h">Non-H</string>
|
||||
<string name="non_h">غير إباحي</string>
|
||||
<string name="asian_porn">إباحيات آسياوية</string>
|
||||
<string name="id">المُعرّف</string>
|
||||
<string name="is_exhentai_gallery">is Exhentai gallery</string>
|
||||
<string name="language_translated">%1$s مترجم</string>
|
||||
<string name="relation_doujinshi">دوجينشي</string>
|
||||
<string name="filename">اسم الملف</string>
|
||||
<string name="file_extension">امتداد الملف</string>
|
||||
<string name="base_url">عنوان URL الأساسي</string>
|
||||
<string name="final_chapter">الفصل الأخير</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">أضف الأحرف الستة الأولى من تجزئة MD5 الخاصة بعنوان URL الخاص بالفصل إلى اسم ملف أو مجلد الفصل.</string>
|
||||
</resources>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<string name="data_saver_server_summary">Put Bandwidth Hero Proxy server url here</string>
|
||||
<string name="clear_db_exclude_read">Keep entries with read chapters</string>
|
||||
<string name="pref_include_chapter_url_hash">Include chapter URL hash</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">Append the first six characters of the chapter URL's MD5 hash to the chapter file folder name.</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">Append the first six characters of the chapter URL's MD5 hash to the chapter file or folder name.</string>
|
||||
|
||||
<!-- Log Level -->
|
||||
<string name="log_minimal">Minimal</string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="action_search_manually">Buscar manualmente</string>
|
||||
<string name="action_search_manually">Búsqueda manual</string>
|
||||
<string name="action_migrate_now">Migrar ahora</string>
|
||||
<string name="action_clean_titles">Limpiar títulos</string>
|
||||
<string name="action_edit_info">Editar información</string>
|
||||
@@ -19,7 +19,7 @@
|
||||
<string name="use_hentai_at_home_option_2">Solo clientes de puerto predeterminado</string>
|
||||
<string name="show_japanese_titles">Mostrar títulos japoneses en los resultados de búsqueda</string>
|
||||
<string name="tag_filtering_threshhold_error">¡Debe estar entre -9999 y 0!</string>
|
||||
<string name="tag_watching_threshhold_error">¡Debe estar entre 0 y 9999!</string>
|
||||
<string name="tag_watching_threshhold_error">Escribe un número entre 0 y 9999</string>
|
||||
<string name="eh_image_quality_1280">1280x</string>
|
||||
<string name="pref_enhanced_e_hentai_view">Exploración E/ExHentai mejorada</string>
|
||||
<string name="action_skip_entry">No migrar</string>
|
||||
@@ -28,7 +28,7 @@
|
||||
<string name="entry_type_webtoon">Webtoon</string>
|
||||
<string name="entry_type_manga">Manga</string>
|
||||
<string name="pref_category_all_sources">Todas las fuentes</string>
|
||||
<string name="pref_category_fork">Ajustes de Bifurcación</string>
|
||||
<string name="pref_category_fork">Ajustes de esta versión de la aplicación</string>
|
||||
<string name="use_hentai_at_home">Use la red Hentai@Home</string>
|
||||
<string name="use_hentai_at_home_summary">Desea cargar imágenes a través de la red de Hentai@Home, si está disponible? Deshabilitar esta opción reducirá la cantidad de páginas que puede ver
|
||||
\nOpciones:
|
||||
@@ -39,27 +39,25 @@
|
||||
<string name="use_original_images">Utiliza imágenes originales</string>
|
||||
<string name="use_original_images_on">Actualmente usando imágenes originales</string>
|
||||
<string name="watched_tags">Etiquetas vistas</string>
|
||||
<string name="language_filtering">Filtrado de idioma</string>
|
||||
<string name="language_filtering">Filtrar idiomas</string>
|
||||
<string name="eh_image_quality_1600">1600x</string>
|
||||
<string name="frong_page_categories">Categorías de la página principal</string>
|
||||
<string name="eh_image_quality_2400">2400x</string>
|
||||
<string name="eh_image_quality_summary">La calidad de las imágenes descargadas</string>
|
||||
<string name="eh_image_quality">Calidad de imagen</string>
|
||||
<string name="eh_image_quality_auto">Auto</string>
|
||||
<string name="eh_image_quality_auto">Automática</string>
|
||||
<string name="eh_image_quality_980">980x</string>
|
||||
<string name="disable_favorites_uploading">Desactivar la subida de favoritos</string>
|
||||
<string name="eh_image_quality_780">780x</string>
|
||||
<string name="show_japanese_titles_option_2">Actualmente se muestran los títulos en inglés/romanizado en los resultados de búsqueda. Borra la caché de capítulos después de cambiar esto (en la sección Avanzado)</string>
|
||||
<string name="watched_tags_summary">Abre una vista web a tu página de etiquetas vistas de E/ExHentai</string>
|
||||
<string name="fromt_page_categories_summary">¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? Aún pueden habilitarse activando sus filtros</string>
|
||||
<string name="language_filtering_summary">Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, selecciónalas en el cuadro de diálogo que aparecerá.
|
||||
\nTen en cuenta que las galerías coincidentes nunca aparecerán, sin importar tu consulta de búsqueda.
|
||||
\nTldr marcado = excluir</string>
|
||||
<string name="fromt_page_categories_summary">¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? El resto se puede seguir viendo activando filtros</string>
|
||||
<string name="language_filtering_summary">Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, márcalas a continuación. \nTen en cuenta que las galerías que marcadas nunca aparecerán, aunque las busques. \nEn pocas palabras: si está marcado = se excluye</string>
|
||||
<string name="watched_tags_exh">Etiquetas Observadas en ExHentai</string>
|
||||
<string name="tag_filtering_threshold">Umbral de Filtrado de Etiquetas</string>
|
||||
<string name="tag_watching_threshhold">Umbral de Monitoreo de Etiquetas</string>
|
||||
<string name="tag_watching_threshhold">Umbral de monitoreo de etiquetas</string>
|
||||
<string name="tag_watching_threshhold_summary">Las galerías recientemente subidas se incluirán en la pantalla de observación si tienen al menos una etiqueta observada con peso positivo, y la suma de los pesos de sus etiquetas observadas alcanza este valor o es mayor. Este umbral se puede establecer entre 0 y 9999. Actualmente: %1$d</string>
|
||||
<string name="watched_list_default">Estado Predeterminado del Filtro de la Lista Observada</string>
|
||||
<string name="watched_list_default">Estado predeterminado del filtro de la lista de favoritos</string>
|
||||
<string name="watched_list_state_summary">Al navegar en ExHentai/E-Hentai, ¿debería estar habilitado el filtro de la lista de seguimiento de forma predeterminada?</string>
|
||||
<string name="pref_enhanced_e_hentai_view_summary">Habilitar/Deshabilitar el menú de navegación mejorado hecho para E/ExHentai</string>
|
||||
<string name="favorites_sync">Sincronización de Favoritos de E-Hentai</string>
|
||||
@@ -67,15 +65,15 @@
|
||||
<string name="disable_favorites_uploading_summary">Los favoritos solo se descargan desde ExHentai. Cualquier cambio en los favoritos en la aplicación no se cargará. Previene la pérdida accidental de favoritos en ExHentai. Ten en cuenta que las eliminaciones aún se descargarán (si eliminas un favorito en ExHentai, también se eliminará en la aplicación).</string>
|
||||
<string name="show_favorite_sync_notes">Mostrar notas sincronizadas de favoritos</string>
|
||||
<string name="show_favorite_sync_notes_summary">Mostrar información sobre la función de sincronización de favoritos</string>
|
||||
<string name="please_login">¡Por favor inicia sesión!</string>
|
||||
<string name="please_login">¡Tienes que iniciar sesión!</string>
|
||||
<string name="ignore_sync_errors">Ignorar errores de sincronización cuando sea posible</string>
|
||||
<string name="force_sync_state_reset">Forzar el restablecimiento del estado de sincronización</string>
|
||||
<string name="sync_state_reset">Restablecimiento del estado de sincronización</string>
|
||||
<string name="force_sync_state_reset">Restablece el estado de sincronización</string>
|
||||
<string name="sync_state_reset">Restablecer estado de sincronización</string>
|
||||
<string name="ignore_sync_errors_summary">No canceles el proceso de sincronización inmediatamente si encuentras errores. Los errores se seguirán mostrando cuando se complete la sincronización. En algunos casos, puede provocar la pérdida de favoritos. Resulta útil cuando se sincronizan bibliotecas grandes.</string>
|
||||
<string name="force_sync_state_reset_summary">Realiza una resincronización completa en la próxima sincronización. Las eliminaciones no se sincronizarán. Todos los favoritos de la aplicación se volverán a cargar en ExHentai y todos los favoritos de ExHentai se volverán a descargar en la aplicación. Útil para reparar la sincronización después de que se haya interrumpido.</string>
|
||||
<string name="gallery_update_checker">Comprobador de actualizaciones de la galería</string>
|
||||
<string name="auto_update_restrictions">Restricciones para actualizaciones automáticas</string>
|
||||
<string name="time_between_batches">Tiempo entre actualizaciones</string>
|
||||
<string name="gallery_update_checker">Comprobar actualizaciones de la galería</string>
|
||||
<string name="auto_update_restrictions">Restringir actualizaciones automáticas</string>
|
||||
<string name="time_between_batches">Intervalo de tiempo entre actualizaciones</string>
|
||||
<string name="time_between_batches_never">Nunca actualizar las galerías</string>
|
||||
<string name="time_between_batches_1_hour">1 hora</string>
|
||||
<string name="time_between_batches_2_hours">2 horas</string>
|
||||
@@ -91,21 +89,19 @@
|
||||
<string name="gallery_updater_stats_text">El actualizador ejecutó %1$s por última vez, y comprobó %2$d de las %3$d galerías que estaban listas para ser comprobadas.</string>
|
||||
<string name="gallery_updater_not_ran_yet">El actualizador aún no se ha ejecutado.</string>
|
||||
<string name="settings_profile_note">Nota de perfil de configuración</string>
|
||||
<string name="eh_settings_successfully_uploaded">¡Ajustes cargados correctamente!</string>
|
||||
<string name="eh_settings_configuration_failed">¡Error en la configuración!</string>
|
||||
<string name="eh_settings_successfully_uploaded">¡Los ajustes se han subido!</string>
|
||||
<string name="eh_settings_configuration_failed">¡Hubo un error en la configuración!</string>
|
||||
<string name="eh_settings_configuration_failed_message">Se ha producido un error durante el proceso de configuración: %1$s</string>
|
||||
<string name="eh_settings_uploading_to_server">Cargar la configuración en el servidor</string>
|
||||
<string name="eh_settings_uploading_to_server">Subiendo ajustes al servidor</string>
|
||||
<string name="time_between_batches_summary_2">%1$s comprueba/actualiza las galerías por lotes. Esto significa que esperará %2$d hora(s), comprobará %3$d galerías, esperará %2$d hora(s), comprobará %3$d y así sucesivamente…</string>
|
||||
<string name="gallery_updater_stats_time">\nGalerías que se comprobaron en:\n- hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- día: %4$d\n- 2 días: %5$d\n- semana: %6$d\n- mes: %7$d\n- año: %8$d</string>
|
||||
<string name="settings_profile_note_message">La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios.
|
||||
\n
|
||||
\nSi no tienes ni idea de lo que son los perfiles de configuración, probablemente no importe, simplemente pulsa \"Aceptar\".</string>
|
||||
<string name="eh_settings_uploading_to_server_message">Por favor espere, esto puede tardar algún tiempo…</string>
|
||||
<string name="eh_settings_out_of_slots_error">¡No tienes espacios en tu perfil %1$s, por favor elimine un perfil!</string>
|
||||
<string name="recheck_login_status">Vuelva a verificar el estado de inicio de sesión</string>
|
||||
<string name="gallery_updater_stats_time">\nGalerías que se comprobaron hace:\n- una hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- un día: %4$d\n- 2 días: %5$d\n- una semana: %6$d\n- un mes: %7$d\n- un año: %8$d</string>
|
||||
<string name="settings_profile_note_message">La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios. \n \nSi no tienes ni idea de lo que son los perfiles de configuración no te preocupes por esto y pulsa «Aceptar».</string>
|
||||
<string name="eh_settings_uploading_to_server_message">Espera un momento, puede tardar un poco…</string>
|
||||
<string name="eh_settings_out_of_slots_error">¡No tienes espacio en tu perfil %1$s, primero elimina un perfil ya existente!</string>
|
||||
<string name="recheck_login_status">Recomprobar estado de inicio de sesión</string>
|
||||
<string name="alternative_login_page">Página de inicio de sesión alternativa</string>
|
||||
<string name="skip_page_restyling">Saltar el cambio de estilo de la página</string>
|
||||
<string name="custom_igneous_cookie">Cookie ígneo personalizado</string>
|
||||
<string name="custom_igneous_cookie">Cookie «igneus» personalizada</string>
|
||||
<string name="custom_igneous_cookie_message">Algunos usuarios no pueden acceder a ExHentai de la manera normal y deben ingresar un valor específico de cookie ígneo. Esta opción es para esos usuarios.</string>
|
||||
<string name="developer_tools">Herramientas de desarrollador</string>
|
||||
<string name="toggle_hentai_features">Activar funciones integradas de hentai</string>
|
||||
@@ -113,51 +109,49 @@
|
||||
<string name="toggle_delegated_sources">Activar fuentes delegadas</string>
|
||||
<string name="toggle_delegated_sources_summary">Aplicar %1$s mejoras a las siguientes fuentes si están instaladas: %2$s</string>
|
||||
<string name="log_level">Nivel de registro</string>
|
||||
<string name="log_level_summary">Cambiar esto puede afectar el rendimiento de la aplicación. Reinicie la aplicación forzosamente después de realizar el cambio. Valor actual: %s</string>
|
||||
<string name="log_level_summary">Cambiar esto puede afectar el rendimiento de la aplicación. Reinicia la aplicación manualmente tras el cambio. Valor actual: %s</string>
|
||||
<string name="enable_source_blacklist">Activar lista negra de fuentes</string>
|
||||
<string name="enable_source_blacklist_summary">Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicie la aplicación forzosamente después de realizar el cambio.</string>
|
||||
<string name="enable_source_blacklist_summary">Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicia la aplicación manualmente tras el cambio.</string>
|
||||
<string name="open_debug_menu">Abrir menú de depuración</string>
|
||||
<string name="open_debug_menu_summary"><![CDATA[¡NO TOQUE ESTE MENÚ A MENOS QUE SEPA LO QUE ESTÁ HACIENDO! <font color=\'red\'>¡PUEDE CORROMPER SU BIBLIOTECA!</font>]]></string>
|
||||
<string name="starting_cleanup">Comenzando la limpieza</string>
|
||||
<string name="open_debug_menu_summary"><![CDATA[¡NO TOQUES ESTE MENÚ A MENOS QUE SEPAS LO QUE HACES! <font color=\'red\'>¡PUEDES CORROMPER TU BIBLIOTECA!</font>]]></string>
|
||||
<string name="starting_cleanup">Comenzando el borrado</string>
|
||||
<string name="clean_up_downloaded_chapters">Limpiar capítulos descargados</string>
|
||||
<string name="delete_unused_chapters">Eliminar carpetas de capítulos inexistentes, parcialmente descargadas y leídas</string>
|
||||
<string name="no_folders_to_cleanup">No hay carpetas para limpiar</string>
|
||||
<string name="clean_orphaned_downloads">Limpiar huérfanos</string>
|
||||
<string name="clean_read_downloads">Limpiar leídos</string>
|
||||
<string name="clean_read_entries_not_in_library">Limpiar entradas no en la biblioteca</string>
|
||||
<string name="delete_unused_chapters">Elimina carpetas de capítulos inexistentes, parcialmente descargados, o ya leídos</string>
|
||||
<string name="no_folders_to_cleanup">No hay ninguna carpeta que limpiar</string>
|
||||
<string name="clean_orphaned_downloads">Borrar descargas huérfanas</string>
|
||||
<string name="clean_read_downloads">Borrar ya leídos</string>
|
||||
<string name="clean_read_entries_not_in_library">Borrar lo que no esté en la biblioteca</string>
|
||||
<string name="data_saver">Ahorro de datos</string>
|
||||
<string name="data_saver_summary">Comprimir imágenes antes de descargarlas o cargarlas en el lector</string>
|
||||
<string name="data_saver_downloader">Usar ahorrador de datos en el descargador</string>
|
||||
<string name="data_saver_summary">Comprimir imágenes antes de descargarlas o verlas en el visor</string>
|
||||
<string name="data_saver_downloader">Descargar con el ahorrador de datos</string>
|
||||
<string name="data_saver_ignore_jpeg">Ignorar imágenes JPEG</string>
|
||||
<string name="data_saver_ignore_gif">Ignorar animaciones GIF</string>
|
||||
<string name="data_saver_image_quality">Calidad de imagen</string>
|
||||
<string name="data_saver_image_format">Comprimir a Jpeg</string>
|
||||
<string name="data_saver_image_format_summary_on">El tamaño del archivo Jpeg es considerablemente más pequeño que el de Webp (lo que significa que se guarda más datos), pero también hace que las imágenes pierdan más calidad.
|
||||
\nActualmente, se comprime a Jpeg</string>
|
||||
<string name="data_saver_image_quality_summary">Valores más altos significan que se guarda un mayor porcentaje de la calidad de la imagen, pero también implica que el tamaño del archivo es mayor; un 80 por ciento es una buena media entre el tamaño del archivo y la calidad de la imagen</string>
|
||||
<string name="data_saver_image_format_summary_off">El tamaño de; archivo JPEG es considerablemente mas pequeño que el WEBP (lo que significa que ser guardan mas datos), pero tambien hace que las imagenes pierdan mucha calidad.
|
||||
\nActualmente se comprimen en WEBP</string>
|
||||
<string name="data_saver_image_format">Comprimir como JPEG</string>
|
||||
<string name="data_saver_image_format_summary_on">El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando JPEG.</string>
|
||||
<string name="data_saver_image_quality_summary">Al poner un porcentaje mayor aumenta la calidad de la imagen, pero también lo hace el tamaño en disco. Dejarlo en 80% es un término medio entre tamaño aceptable y con calidad.</string>
|
||||
<string name="data_saver_image_format_summary_off">El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando WEBP.</string>
|
||||
<string name="data_saver_color_bw">Convertir en Blanco & Negro</string>
|
||||
<string name="bandwidth_hero">Bandwidth Hero ( Requiere un servidor proxy )</string>
|
||||
<string name="wsrv">wsrv.nl</string>
|
||||
<string name="data_saver_server_summary">Pom aqui la URl del Servidor Proxy de Bandwidth Hero</string>
|
||||
<string name="data_saver_server_summary">Pega aquí la dirección URL del servidor proxy de Bandwidth Hero</string>
|
||||
<string name="clear_db_exclude_read">Conservar obras con capítulos leídos</string>
|
||||
<string name="bandwidth_data_saver_server">Servidor Proxy de Bandwidth Hero</string>
|
||||
<string name="log_minimal">Minimo</string>
|
||||
<string name="log_minimal">Mínimo</string>
|
||||
<string name="log_extra">Extra</string>
|
||||
<string name="log_extreme">Extremo</string>
|
||||
<string name="log_minimal_desc">Solo errores criticos</string>
|
||||
<string name="log_extra_desc">Todos los registros</string>
|
||||
<string name="log_extreme_desc">Modo de inspeccion de red</string>
|
||||
<string name="log_minimal_desc">Solo errores críticos</string>
|
||||
<string name="log_extra_desc">Registrar todos los mensajes</string>
|
||||
<string name="log_extreme_desc">Modo de inspección de red</string>
|
||||
<string name="toggle_expand_search_filters">Ampliar todos los filtros de búsqueda de forma predeterminada</string>
|
||||
<string name="pref_previews_row_count">Vista previa del recuento de filas</string>
|
||||
<string name="pref_previews_row_count">Filas en vista previa</string>
|
||||
<string name="pref_hide_updates_button">Mostrar actualizaciones en la barra de navegación</string>
|
||||
<string name="pref_hide_history_button">Mostrar historial en la barra de navegación</string>
|
||||
<string name="pref_show_bottom_bar_labels">Mostrar siempre las etiquetas de navegación</string>
|
||||
<string name="pref_category_navbar">Barra de navegación</string>
|
||||
<string name="put_recommends_in_overflow">Mostrar recomendaciones en el menú</string>
|
||||
<string name="put_recommends_in_overflow_summary">Coloque el botón de recomendaciones en el menú adicional en lugar de en la página de entrada</string>
|
||||
<string name="put_merge_in_overflow">Fusionar en el menú adicional</string>
|
||||
<string name="put_recommends_in_overflow">Recomendaciones en menú lateral</string>
|
||||
<string name="put_recommends_in_overflow_summary">Poner el botón de recomendaciones en el menú lateral en vez de en la página de la obra</string>
|
||||
<string name="put_merge_in_overflow">Fusionar en el menú lateral</string>
|
||||
<string name="put_merge_in_overflow_summary">Coloque el botón de fusión en el menú adicional en lugar de en la página de entrada</string>
|
||||
<string name="pref_sorting_settings">Ajustes de ordenación</string>
|
||||
<string name="pref_skip_pre_migration_summary">Utilizar las últimas preferencias y fuentes guardadas antes de la migración para migrar</string>
|
||||
@@ -168,7 +162,7 @@
|
||||
<string name="pref_library_mark_duplicate_chapters">Marcar nuevos capítulos duplicados como leídos</string>
|
||||
<string name="pref_library_mark_duplicate_chapters_summary">Marcar automáticamente nuevos capítulos como leídos si se han leído antes</string>
|
||||
<string name="update_1hour">Cada hora</string>
|
||||
<string name="pref_hide_feed">Ocultar pestaña Feed</string>
|
||||
<string name="pref_hide_feed">Ocultar pestaña de novedades</string>
|
||||
<string name="pref_source_source_filtering_summery">Filtrar las fuentes que están en categorías, haciendo que las fuentes no se pongan debajo del idioma si están en una categoría</string>
|
||||
<string name="pref_source_navigation_summery">Reemplace el botón más reciente con una vista de exploración personalizada que incluya tanto lo más reciente como la exploración</string>
|
||||
<string name="all_read_entries">Todas las obras leídas</string>
|
||||
@@ -177,8 +171,8 @@
|
||||
<string name="update_30min">Cada 30 minutos</string>
|
||||
<string name="pref_source_source_filtering">Filtrar las fuentes en categorías</string>
|
||||
<string name="update_3hour">Cada 3 horas</string>
|
||||
<string name="pref_feed_position">Posición de la pestaña Feed</string>
|
||||
<string name="pref_feed_position_summery">¿Quieres que la pestaña feed sea la primera pestaña en navegar? Esto hará que sea la pestaña predeterminada al abrir la navegación, no se recomienda si está con datos móviles o una red medida</string>
|
||||
<string name="pref_feed_position">Posición de la pestaña de novedades</string>
|
||||
<string name="pref_feed_position_summery">¿Quieres que la pestaña de novedades sea la inicial? No te recomendamos que lo actives si tienes datos móviles o datos limitados</string>
|
||||
<string name="pref_source_navigation">Reemplazar el botón más reciente</string>
|
||||
<string name="pref_local_source_hidden_folders">Carpetas ocultas de fuente local</string>
|
||||
<string name="pref_local_source_hidden_folders_summery">Permite a la fuente local leer carpetas ocultas</string>
|
||||
@@ -217,10 +211,10 @@
|
||||
<string name="action_edit_biometric_lock_times">Editar horarios de bloqueo</string>
|
||||
<string name="biometric_lock_times_empty">No tienes horarios de bloqueo biométrico. Pulsa el botón «+» para añadir uno.</string>
|
||||
<string name="biometric_lock_time_conflicts">¡Una hora de bloqueo entra en conflicto con otra existente!</string>
|
||||
<string name="biometric_lock_start_time">Introducir hora de inicio</string>
|
||||
<string name="biometric_lock_end_time">Introducir hora de finalización</string>
|
||||
<string name="biometric_lock_start_time">Introduce una hora de inicio</string>
|
||||
<string name="biometric_lock_end_time">Introduce una hora de finalización</string>
|
||||
<string name="delete_time_range">Eliminar intervalo de tiempo</string>
|
||||
<string name="delete_time_range_confirmation">¿Deseas eliminar el intervalo de tiempo %s?</string>
|
||||
<string name="delete_time_range_confirmation">¿Quieres borrar el intervalo de tiempo %s?</string>
|
||||
<string name="biometric_lock_days">Días de bloqueo biométrico</string>
|
||||
<string name="biometric_lock_days_summary">Días para tener la aplicación bloqueada</string>
|
||||
<string name="sunday">Domingo</string>
|
||||
@@ -242,7 +236,7 @@
|
||||
<string name="thursday">Jueves</string>
|
||||
<string name="encrypt_database">Cifrar base de datos</string>
|
||||
<string name="friday">Viernes</string>
|
||||
<string name="encrypt_database_subtitle">Requiere reiniciar la aplicación para que surta efecto</string>
|
||||
<string name="encrypt_database_subtitle">Es necesario reiniciar la aplicación para que surta efecto</string>
|
||||
<string name="encrypt_database_message"><![CDATA[<font color=\'red\'>ACTIVAR ESTO CREARÁ UNA NUEVA BASE DE DATOS. USA ESTOS PASOS PARA MANTENER TUS DATOS<br>1. AJUSTES -> COPIA DE SEGURIDAD -> CREAR<br>2. AJUSTES DEL SISTEMA -> BORRAR LOS DATOS DE LA APLICACIÓN<br>3. ABRIR LA APLICACIÓN Y ACTIVAR ESTO<br>4. AJUSTES DEL SISTEMA -> FORZAR REINICIO<br>5. AJUSTES -> COPIA DE SEGURIDAD -> RESTAURAR</font>]]></string>
|
||||
<string name="password_protect_downloads">Descargas protegidas por contraseña</string>
|
||||
<string name="password_protect_downloads_summary">Encripta las descargas de archivos CBZ con la contraseña dada.\nADVERTENCIA: LOS DATOS DE LOS ARCHIVOS SE PERDERÁN PARA SIEMPRE SI SE OLVIDA LA CONTRASEÑA</string>
|
||||
@@ -435,7 +429,7 @@
|
||||
<string name="favorites_sync_removing_galleries">Eliminando %1$d galerías del servidor remoto</string>
|
||||
<string name="favorites_sync_unable_to_delete">¡No se pudo eliminar las galerías del servidor remoto!</string>
|
||||
<string name="tracking_status">Estado de seguimiento</string>
|
||||
<string name="not_tracked">Sin seguir</string>
|
||||
<string name="not_tracked">Sin seguimiento</string>
|
||||
<string name="sync_favorites">Sincronizar favoritos de EH</string>
|
||||
<string name="favorites_sync_reset">¿Estás seguro?</string>
|
||||
<string name="favorites_sync_reset_message">Restablecer el estado de sincronización puede hacer que la próxima sincronización sea extremadamente lenta.</string>
|
||||
@@ -489,7 +483,7 @@
|
||||
<string name="relation_spin_off">Derivado de</string>
|
||||
<string name="relation_alternate_story">Historia alternativa</string>
|
||||
<string name="relation_alternate_version">Versión alternativa</string>
|
||||
<string name="feed_add">¿Añadir %1$s al feed?</string>
|
||||
<string name="feed_add">¿Añadir %1$s a novedades?</string>
|
||||
<string name="error_with_reason">Error: %1$s</string>
|
||||
<string name="could_not_open_entry">No se pudo abrir esta entrada:\n\n%1$s</string>
|
||||
<string name="launching_app">Iniciando aplicación…</string>
|
||||
@@ -548,12 +542,12 @@
|
||||
<string name="pref_crop_borders_continuous_vertical">Recortar el borde vertical</string>
|
||||
<string name="humanize_fallback">hace unos instantes</string>
|
||||
<string name="pref_crop_borders_webtoon">Recortar bordes Webtoon</string>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="feed_delete">¿Borrar artículo de feed?</string>
|
||||
<string name="too_many_in_feed">Demasiadas fuentes en tu feed, no se pueden agregar más de 10</string>
|
||||
<string name="feed">Novedades</string>
|
||||
<string name="feed_delete">¿Borrar el elemento de novedades?</string>
|
||||
<string name="too_many_in_feed">Demasiadas fuentes en novedades, no se pueden poner más de 10</string>
|
||||
<string name="action_add_tags_message">¡Lee esto! ¡Etiquetas deben ser exactas, no hay coincidencias parciales, no puedes hacer netorare para excluir mujer:netorare o similar!\nEl estilo para etiquetas de nombre es\n\"mujer: solo mujer\"\n¡sin citas!\n¡Se pueden añadir multiples variantes de la misma etiqueta, así que puedes hacer \"etiqueta:netorare\" para NHentai y \"mujer:netorare\" para E-Hentai!</string>
|
||||
<string name="select_none">Selecciona ninguno</string>
|
||||
<string name="feed_tab_empty">No tiene fuentes en su feed, navegar a la parte superior derecha para añadir una</string>
|
||||
<string name="feed_tab_empty">No tienes ninguna fuente en novedades, añade alguna desde la parte superior derecha</string>
|
||||
<string name="skip_pre_migration">Saltar pre-migración</string>
|
||||
<string name="search_parameter">Buscar parámetro (p. ej. idioma:inglés)</string>
|
||||
<string name="lewd">Lascivo</string>
|
||||
@@ -584,7 +578,7 @@
|
||||
<string name="page_count">Número de páginas</string>
|
||||
<string name="parent">Padre</string>
|
||||
<string name="uploader">Cargador</string>
|
||||
<string name="url">URL</string>
|
||||
<string name="url">Dirección URL</string>
|
||||
<string name="uploader_capital">Cargador principal</string>
|
||||
<string name="follow_status">Seguir estado</string>
|
||||
<string name="language_translated">%1$s TR</string>
|
||||
@@ -610,7 +604,7 @@
|
||||
<string name="relation_monochrome">Monocromo</string>
|
||||
<string name="entry_not_tracked">El título no tiene seguimiento.</string>
|
||||
<string name="select_tracker">Elige un servicio de seguimiento</string>
|
||||
<string name="fill_from_tracker">Rellenar desde el serv. de seguimiento</string>
|
||||
<string name="fill_from_tracker">Rellenar desde seguim.</string>
|
||||
<string name="favorites_sync_unable_to_add_to_remote">No se puede añadir la galería al servidor remoto: \'%1$s\' (GID: %2$s)!</string>
|
||||
<string name="rec_error_title">La búsqueda no se ha podido completar</string>
|
||||
<string name="rec_common_recommendations">Recomendaciones en común</string>
|
||||
@@ -626,10 +620,13 @@
|
||||
<string name="rec_group_source">Recomendaciones de la fuente</string>
|
||||
<string name="rec_services_to_search">Servicios de recomendación en los que buscar</string>
|
||||
<string name="rec_hide_library_entries">Ocultar elementos que ya estén en la biblioteca</string>
|
||||
<string name="pref_tracker_resolve_using_source_metadata_summary">Encuentra el título automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex</string>
|
||||
<string name="pref_tracker_resolve_using_source_metadata">Elegir títulos a través de los metadatos de la fuente</string>
|
||||
<string name="pref_tracker_resolve_using_source_metadata_summary">Encuentra la obra automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex</string>
|
||||
<string name="pref_tracker_resolve_using_source_metadata">Elegir obras a través de los metadatos de la fuente</string>
|
||||
<string name="scan_qr_code">Escanear un código QR</string>
|
||||
<string name="final_chapter">Capítulo final</string>
|
||||
<string name="file_extension">Extensión de archivo</string>
|
||||
<string name="base_url">URL base</string>
|
||||
<string name="base_url">Dirección URL base</string>
|
||||
<string name="filename">Nombre de archivo</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">Añade las primeras seis letras del hash MD5 de la dirección URL del capítulo al nombre de archivo o carpeta local.</string>
|
||||
<string name="pref_include_chapter_url_hash">Incluir el hash de la URL del capítulo</string>
|
||||
</resources>
|
||||
|
||||
@@ -651,4 +651,6 @@
|
||||
<string name="file_extension">Extension ng file</string>
|
||||
<string name="final_chapter">Huling Kabanata</string>
|
||||
<string name="base_url">Base url</string>
|
||||
<string name="pref_include_chapter_url_hash">Isama ang hash ng URL ng kabanata</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">Idagdag ang unang anim na karakter ng MD5 hash ng URL ng kabanata sa pangalan ng file o folder ng kabanata.</string>
|
||||
</resources>
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
<string name="pref_sorting_settings">Paramètres de tri</string>
|
||||
<string name="pref_skip_pre_migration_summary">Utilisez les dernières préférences et sources de pré-migration enregistrées pour migrer en masse</string>
|
||||
<string name="library_group_updates">Mises à jour des catégories dynamiques de la bibliothèque</string>
|
||||
<string name="library_group_updates_global">Lancez toujours les mises à jour mondiales</string>
|
||||
<string name="library_group_updates_all_but_ungrouped">Lancer des mises à jour globales uniquement pour les mises à jour de catégories non groupées pour les autres</string>
|
||||
<string name="library_group_updates_global">Toujours lancer des mises à jour globales</string>
|
||||
<string name="library_group_updates_all_but_ungrouped">Lancer des mises à jour globales uniquement pour les entrées non regroupées, lancer des mises à jour par catégorie pour les autres</string>
|
||||
<string name="library_group_updates_all">Lancer des mises à jour de catégorie tout le temps</string>
|
||||
<!-- Browse settings -->
|
||||
<string name="pref_source_navigation">Remplacer le dernier bouton</string>
|
||||
@@ -669,4 +669,10 @@
|
||||
<string name="mangadex_push_favorites_to_mangadex_summary">Synchronise toutes les séries non suivies par MdList vers MangaDex en tant que lecture en cours.</string>
|
||||
<string name="similar_titles">Titres similaires</string>
|
||||
<string name="select_scanlators">Groupes de scantrad à afficher</string>
|
||||
<string name="pref_include_chapter_url_hash">Inclure le hash de l\'URL du chapitre</string>
|
||||
<string name="pref_include_chapter_url_hash_desc">Ajoutez les six premiers caractères du hachage MD5 de l\'URL du chapitre au nom du fichier ou du dossier du chapitre.</string>
|
||||
<string name="filename">Nom du fichier</string>
|
||||
<string name="file_extension">Extension du fichier</string>
|
||||
<string name="base_url">URL de base</string>
|
||||
<string name="final_chapter">Chapitre final</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user