Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -100,5 +100,5 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
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
|
required: true
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
run: ./gradlew spotlessCheck assembleDevDebug
|
run: ./gradlew spotlessCheck assembleDevDebug
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: TachiyomiSY-${{ github.sha }}.apk
|
name: TachiyomiSY-${{ github.sha }}.apk
|
||||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||||
|
|||||||
@@ -150,12 +150,14 @@ kotlin {
|
|||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-Xannotation-default-target=param-property",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +267,7 @@ dependencies {
|
|||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
implementation(libs.bundles.markdown)
|
implementation(libs.bundles.markdown)
|
||||||
|
implementation(libs.materialKolor)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
|||||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||||
import tachiyomi.domain.category.repository.CategoryRepository
|
import tachiyomi.domain.category.repository.CategoryRepository
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
@@ -156,6 +157,7 @@ class DomainModule : InjektModule {
|
|||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
addFactory { GetChapter(get()) }
|
addFactory { GetChapter(get()) }
|
||||||
addFactory { GetChaptersByMangaId(get()) }
|
addFactory { GetChaptersByMangaId(get()) }
|
||||||
|
addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) }
|
||||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
|
||||||
|
|||||||
+2
-2
@@ -3,12 +3,12 @@ package eu.kanade.presentation.browse.components
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -17,7 +17,7 @@ fun BrowseSourceFloatingActionButton(
|
|||||||
onFabClick: () -> Unit,
|
onFabClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
SmallExtendedFloatingActionButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = {
|
text = {
|
||||||
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.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
|||||||
onCreate: () -> Unit,
|
onCreate: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
SmallExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||||
onClick = onCreate,
|
onClick = onCreate,
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
import androidx.compose.material3.TooltipBox
|
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.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
|||||||
|
|
||||||
actions.filterIsInstance<AppBar.Action>().map {
|
actions.filterIsInstance<AppBar.Action>().map {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(it.title)
|
Text(it.title)
|
||||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
|||||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||||
if (overflowActions.isNotEmpty()) {
|
if (overflowActions.isNotEmpty()) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
|||||||
// Don't show search action
|
// Don't show search action
|
||||||
} else if (searchQuery == null) {
|
} else if (searchQuery == null) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_search))
|
Text(stringResource(MR.strings.action_search))
|
||||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
} else if (searchQuery.isNotEmpty()) {
|
} else if (searchQuery.isNotEmpty()) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_reset))
|
Text(stringResource(MR.strings.action_reset))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadDropdownMenu(
|
fun DownloadDropdownMenu(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
offset: DpOffset? = null,
|
offset: DpOffset? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
if (offset != null) {
|
if (offset != null) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
private fun DownloadDropdownMenuItems(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> 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_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
|
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||||
)
|
)
|
||||||
|
|
||||||
options.map { (downloadAction, string) ->
|
options.map { (downloadAction, string) ->
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.presentation.manga
|
package eu.kanade.presentation.manga
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -101,7 +100,6 @@ import tachiyomi.domain.source.model.StubSource
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
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.PullRefresh
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -167,7 +165,7 @@ fun MangaScreen(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -331,7 +329,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -418,25 +416,23 @@ private fun MangaScreenSmallImpl(
|
|||||||
val isFABVisible = remember(chapters) {
|
val isFABVisible = remember(chapters) {
|
||||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
SmallExtendedFloatingActionButton(
|
||||||
visible = isFABVisible,
|
text = {
|
||||||
enter = fadeIn(),
|
val isReading = remember(state.chapters) {
|
||||||
exit = fadeOut(),
|
state.chapters.fastAny { it.chapter.read }
|
||||||
) {
|
}
|
||||||
ExtendedFloatingActionButton(
|
Text(
|
||||||
text = {
|
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||||
val isReading = remember(state.chapters) {
|
)
|
||||||
state.chapters.fastAny { it.chapter.read }
|
},
|
||||||
}
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
Text(
|
onClick = onContinueReading,
|
||||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
},
|
visible = isFABVisible,
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
alignment = Alignment.BottomEnd,
|
||||||
onClick = onContinueReading,
|
),
|
||||||
expanded = chapterListState.shouldExpandFAB(),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val topPadding = contentPadding.calculateTopPadding()
|
val topPadding = contentPadding.calculateTopPadding()
|
||||||
@@ -654,7 +650,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -737,27 +733,25 @@ fun MangaScreenLargeImpl(
|
|||||||
val isFABVisible = remember(chapters) {
|
val isFABVisible = remember(chapters) {
|
||||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
SmallExtendedFloatingActionButton(
|
||||||
visible = isFABVisible,
|
text = {
|
||||||
enter = fadeIn(),
|
val isReading = remember(state.chapters) {
|
||||||
exit = fadeOut(),
|
state.chapters.fastAny { it.chapter.read }
|
||||||
) {
|
}
|
||||||
ExtendedFloatingActionButton(
|
Text(
|
||||||
text = {
|
text = stringResource(
|
||||||
val isReading = remember(state.chapters) {
|
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||||
state.chapters.fastAny { it.chapter.read }
|
),
|
||||||
}
|
)
|
||||||
Text(
|
},
|
||||||
text = stringResource(
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
onClick = onContinueReading,
|
||||||
),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
},
|
visible = isFABVisible,
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
alignment = Alignment.BottomEnd,
|
||||||
onClick = onContinueReading,
|
),
|
||||||
expanded = chapterListState.shouldExpandFAB(),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
@@ -953,7 +947,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
// SY <--
|
// SY <--
|
||||||
onChapterClicked: (Chapter) -> Unit,
|
onChapterClicked: (Chapter) -> Unit,
|
||||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> 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,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
@@ -1020,14 +1014,14 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onChapterSelected(item, !item.selected, true, true)
|
onChapterSelected(item, !item.selected, true)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onChapterItemClick(
|
onChapterItemClick(
|
||||||
chapterItem = item,
|
chapterItem = item,
|
||||||
isAnyChapterSelected = isAnyChapterSelected,
|
isAnyChapterSelected = isAnyChapterSelected,
|
||||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
|||||||
NEXT_10_CHAPTERS,
|
NEXT_10_CHAPTERS,
|
||||||
NEXT_25_CHAPTERS,
|
NEXT_25_CHAPTERS,
|
||||||
UNREAD_CHAPTERS,
|
UNREAD_CHAPTERS,
|
||||||
|
BOOKMARKED_CHAPTERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class EditCoverAction {
|
enum class EditCoverAction {
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ fun MangaBottomActionMenu(
|
|||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
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 ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
@@ -260,10 +260,10 @@ fun LibraryBottomActionMenu(
|
|||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
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 ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
|
|||||||
@@ -605,44 +605,47 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
@Composable
|
||||||
annotate = { content, child ->
|
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
markdownAnnotator(
|
||||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
annotate = { content, child ->
|
||||||
|
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||||
|
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||||
|
|
||||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||||
?.getUnescapedTextInNode(content)
|
|
||||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
|
||||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
|
||||||
?.getUnescapedTextInNode(content)
|
?.getUnescapedTextInNode(content)
|
||||||
?: return@markdownAnnotator false
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||||
|
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: return@markdownAnnotator false
|
||||||
|
|
||||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||||
?.getUnescapedTextInNode(content).orEmpty()
|
?.getUnescapedTextInNode(content).orEmpty()
|
||||||
|
|
||||||
withLink(LinkAnnotation.Url(url = url)) {
|
withLink(LinkAnnotation.Url(url = url)) {
|
||||||
pushStyle(linkStyle)
|
pushStyle(linkStyle)
|
||||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||||
append(altText)
|
append(altText)
|
||||||
pop()
|
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) {
|
false
|
||||||
append(content.substring(child.startOffset, child.endOffset))
|
},
|
||||||
return@markdownAnnotator true
|
config = markdownAnnotatorConfig(
|
||||||
}
|
eolAsNewLine = true,
|
||||||
|
),
|
||||||
false
|
)
|
||||||
},
|
}
|
||||||
config = markdownAnnotatorConfig(
|
|
||||||
eolAsNewLine = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ object AboutScreen : Screen() {
|
|||||||
is GetApplicationRelease.Result.OsTooOld -> {
|
is GetApplicationRelease.Result.OsTooOld -> {
|
||||||
context.toast(MR.strings.update_check_eol)
|
context.toast(MR.strings.update_check_eol)
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toast(e.message)
|
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.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
|
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||||
LibrariesContainer(
|
LibrariesContainer(
|
||||||
|
libraries = libraries,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
|||||||
+2
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ class DebugInfoScreen : Screen() {
|
|||||||
val status by produceState(initialValue = "-") {
|
val status by produceState(initialValue = "-") {
|
||||||
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
|
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
|
||||||
value = when (result) {
|
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 -> "Compiled"
|
||||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
|
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
|
||||||
"Compiled non-matching"
|
"Compiled non-matching"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package eu.kanade.presentation.theme
|
package eu.kanade.presentation.theme
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialExpressiveTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
|||||||
isAmoled: Boolean,
|
isAmoled: Boolean,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
val context = LocalContext.current
|
||||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
val isDark = isSystemInDarkTheme()
|
||||||
|
MaterialExpressiveTheme(
|
||||||
|
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||||
|
getThemeColorScheme(
|
||||||
|
context = context,
|
||||||
|
appTheme = appTheme,
|
||||||
|
isDark = isDark,
|
||||||
|
isAmoled = isAmoled,
|
||||||
|
)
|
||||||
|
},
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
private fun getThemeColorScheme(
|
private fun getThemeColorScheme(
|
||||||
|
context: Context,
|
||||||
appTheme: AppTheme,
|
appTheme: AppTheme,
|
||||||
|
isDark: Boolean,
|
||||||
isAmoled: Boolean,
|
isAmoled: Boolean,
|
||||||
): ColorScheme {
|
): ColorScheme {
|
||||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||||
MonetColorScheme(LocalContext.current)
|
MonetColorScheme(context)
|
||||||
} else {
|
} else {
|
||||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||||
}
|
}
|
||||||
return colorScheme.getColorScheme(
|
return colorScheme.getColorScheme(
|
||||||
isSystemInDarkTheme(),
|
isDark = isDark,
|
||||||
isAmoled,
|
isAmoled = isAmoled,
|
||||||
|
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
|||||||
private val surfaceContainerHigh = Color(0xFF131313)
|
private val surfaceContainerHigh = Color(0xFF131313)
|
||||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
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 (!isDark) return lightScheme
|
||||||
|
|
||||||
if (!isAmoled) return darkScheme
|
if (!isAmoled) return darkScheme
|
||||||
|
|
||||||
return darkScheme.copy(
|
val amoledScheme = darkScheme.copy(
|
||||||
background = Color.Black,
|
background = Color.Black,
|
||||||
onBackground = Color.White,
|
onBackground = Color.White,
|
||||||
surface = Color.Black,
|
surface = Color.Black,
|
||||||
onSurface = Color.White,
|
onSurface = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||||
|
|
||||||
|
return amoledScheme.copy(
|
||||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||||
surfaceContainerLowest = surfaceContainer,
|
surfaceContainerLowest = surfaceContainer,
|
||||||
surfaceContainerLow = surfaceContainer,
|
surfaceContainerLow = surfaceContainer,
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
package eu.kanade.presentation.theme.colorscheme
|
package eu.kanade.presentation.theme.colorscheme
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.UiModeManager
|
|
||||||
import android.app.WallpaperManager
|
import android.app.WallpaperManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.content.getSystemService
|
import com.materialkolor.PaletteStyle
|
||||||
import com.google.android.material.color.utilities.Hct
|
import com.materialkolor.dynamiccolor.ColorSpec
|
||||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
import com.materialkolor.ktx.DynamicScheme
|
||||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
import com.materialkolor.toColorScheme
|
||||||
import com.google.android.material.color.utilities.SchemeContent
|
|
||||||
import com.google.android.material.color.utilities.Score
|
|
||||||
|
|
||||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||||
|
|
||||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
?.primaryColor
|
?.primaryColor
|
||||||
?.toArgb()
|
?.toArgb()
|
||||||
if (seed != null) {
|
if (seed != null) {
|
||||||
MonetCompatColorScheme(context, seed)
|
MonetCompatColorScheme(Color(seed))
|
||||||
} else {
|
} else {
|
||||||
TachiyomiColorScheme
|
TachiyomiColorScheme
|
||||||
}
|
}
|
||||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
|
|
||||||
override val lightScheme
|
override val lightScheme
|
||||||
get() = monet.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)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
override val darkScheme = dynamicDarkColorScheme(context)
|
override val darkScheme = dynamicDarkColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||||
|
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun Int.toComposeColor(): Color = Color(this)
|
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||||
|
return DynamicScheme(
|
||||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
seedColor = seed,
|
||||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
isDark = dark,
|
||||||
val scheme = SchemeContent(
|
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||||
Hct.fromInt(seed),
|
style = PaletteStyle.Expressive,
|
||||||
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(),
|
|
||||||
)
|
)
|
||||||
|
.toColorScheme(isAmoled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.track
|
package eu.kanade.presentation.track
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.Clipboard
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
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.capitalize
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.intl.Locale
|
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.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboard: Clipboard = LocalClipboard.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||||
val status = trackSearch.publishing_status.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 shape = RoundedCornerShape(16.dp)
|
||||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
|||||||
expanded = dropDownMenuExpanded,
|
expanded = dropDownMenuExpanded,
|
||||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||||
onCopyName = {
|
onCopyName = {
|
||||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
scope.launch {
|
||||||
|
val clipEntry = ClipData.newPlainText(
|
||||||
|
trackSearch.title,
|
||||||
|
trackSearch.title,
|
||||||
|
).toClipEntry()
|
||||||
|
clipboard.setClipEntry(clipEntry)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onOpenInBrowser = {
|
onOpenInBrowser = {
|
||||||
val url = trackSearch.tracking_url
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
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.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
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.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.theme.active
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -57,8 +61,10 @@ fun UpdateScreen(
|
|||||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
|
onFilterClicked: () -> Unit,
|
||||||
|
hasActiveFilters: Boolean,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode) {
|
BackHandler(enabled = state.selectionMode) {
|
||||||
onSelectAll(false)
|
onSelectAll(false)
|
||||||
@@ -69,6 +75,8 @@ fun UpdateScreen(
|
|||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
onCalendarClicked = { onCalendarClicked() },
|
onCalendarClicked = { onCalendarClicked() },
|
||||||
onUpdateLibrary = { onUpdateLibrary() },
|
onUpdateLibrary = { onUpdateLibrary() },
|
||||||
|
onFilterClicked = { onFilterClicked() },
|
||||||
|
hasFilters = hasActiveFilters,
|
||||||
actionModeCounter = state.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { onSelectAll(true) },
|
onSelectAll = { onSelectAll(true) },
|
||||||
onInvertSelection = { onInvertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
@@ -139,6 +147,8 @@ fun UpdateScreen(
|
|||||||
private fun UpdatesAppBar(
|
private fun UpdatesAppBar(
|
||||||
onCalendarClicked: () -> Unit,
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Unit,
|
onUpdateLibrary: () -> Unit,
|
||||||
|
onFilterClicked: () -> Unit,
|
||||||
|
hasFilters: Boolean,
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
@@ -153,6 +163,12 @@ private fun UpdatesAppBar(
|
|||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
persistentListOf(
|
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(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_view_upcoming),
|
title = stringResource(MR.strings.action_view_upcoming),
|
||||||
icon = Icons.Outlined.CalendarMonth,
|
icon = Icons.Outlined.CalendarMonth,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ internal fun LazyListScope.updatesUiItems(
|
|||||||
// SY -->
|
// SY -->
|
||||||
preserveReadingPosition: Boolean,
|
preserveReadingPosition: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onClickUpdate: (UpdatesItem) -> Unit,
|
onClickUpdate: (UpdatesItem) -> Unit,
|
||||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||||
@@ -120,11 +120,11 @@ internal fun LazyListScope.updatesUiItems(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
when {
|
when {
|
||||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||||
else -> onClickUpdate(updatesItem)
|
else -> onClickUpdate(updatesItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
context(Context)
|
context(context: Context)
|
||||||
val Throwable.formattedMessage: String
|
val Throwable.formattedMessage: String
|
||||||
get() {
|
get() {
|
||||||
when (this) {
|
when (this) {
|
||||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||||
is UnknownHostException -> {
|
is UnknownHostException -> {
|
||||||
return if (!isOnline()) {
|
return if (!context.isOnline()) {
|
||||||
stringResource(MR.strings.exception_offline)
|
context.stringResource(MR.strings.exception_offline)
|
||||||
} else {
|
} 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 NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||||
}
|
}
|
||||||
return when (val className = this::class.simpleName) {
|
return when (val className = this::class.simpleName) {
|
||||||
"Exception", "IOException" -> message ?: className
|
"Exception", "IOException" -> message ?: className
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
// https://issuetracker.google.com/352584409
|
// https://issuetracker.google.com/352584409
|
||||||
context(LazyItemScope)
|
context(itemScope: LazyItemScope)
|
||||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||||
|
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ fun EhLoginWebViewScreen(
|
|||||||
)
|
)
|
||||||
is LoadingState.Loading -> {
|
is LoadingState.Loading -> {
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedProgress by animateFloatAsState(
|
||||||
(loadingState as? LoadingState.Loading)?.progress ?: 1f,
|
loadingState.progress,
|
||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
label = "webview_loading",
|
label = "webview_loading",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ fun WebViewScreenContent(
|
|||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
)
|
)
|
||||||
is LoadingState.Loading -> LinearProgressIndicator(
|
is LoadingState.Loading -> LinearProgressIndicator(
|
||||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
progress = { loadingState.progress },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
|
|||||||
@@ -420,7 +420,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
is SourceNotInstalledException -> context.stringResource(
|
is SourceNotInstalledException -> context.stringResource(
|
||||||
MR.strings.loader_not_implemented_error,
|
MR.strings.loader_not_implemented_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> e.message
|
else -> e.message
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
@@ -692,8 +691,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {}
|
||||||
}
|
|
||||||
return File("")
|
return File("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,10 +735,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
const val KEY_GROUP_EXTRA = "group_extra"
|
const val KEY_GROUP_EXTRA = "group_extra"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun cancelAllWorks(context: Context) {
|
|
||||||
context.workManager.cancelAllWorkByTag(TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupTask(
|
fun setupTask(
|
||||||
context: Context,
|
context: Context,
|
||||||
prefInterval: Int? = null,
|
prefInterval: Int? = null,
|
||||||
@@ -754,16 +748,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
} else {
|
} else {
|
||||||
NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
}
|
}
|
||||||
val networkRequestBuilder = NetworkRequest.Builder()
|
val networkRequest = NetworkRequest.Builder().apply {
|
||||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||||
}
|
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
}
|
||||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||||
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.build()
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
.setRequiresBatteryNotLow(true)
|
.setRequiresBatteryNotLow(true)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.await
|
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.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
@@ -34,7 +40,26 @@ class SyncYomiSyncService(
|
|||||||
|
|
||||||
private class SyncYomiException(message: String?) : Exception(message)
|
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? {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (remoteData, etag) = pullSyncData()
|
val (remoteData, etag) = pullSyncData()
|
||||||
|
|
||||||
@@ -52,11 +77,23 @@ class SyncYomiSyncService(
|
|||||||
syncData
|
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
|
return finalSyncData.backup
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||||
notifier.showSyncError(e.message)
|
notifier.showSyncError(e.message)
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +160,8 @@ class SyncYomiSyncService(
|
|||||||
/**
|
/**
|
||||||
* Return true if update success
|
* Return true if update success
|
||||||
*/
|
*/
|
||||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
||||||
val backup = syncData.backup ?: return
|
val backup = syncData.backup ?: return true
|
||||||
|
|
||||||
val host = syncPreferences.clientHost().get()
|
val host = syncPreferences.clientHost().get()
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
@@ -163,13 +200,49 @@ class SyncYomiSyncService(
|
|||||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||||
syncPreferences.lastSyncEtag().set(newETag)
|
syncPreferences.lastSyncEtag().set(newETag)
|
||||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||||
|
return true
|
||||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||||
// other clients updated remote data, will try next time
|
// other clients updated remote data, will try next time
|
||||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||||
|
return false
|
||||||
} else {
|
} else {
|
||||||
val responseBody = response.body.string()
|
val responseBody = response.body.string()
|
||||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||||
logcat(LogPriority.ERROR) { "SyncError: $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 {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
|
||||||
| id
|
| id
|
||||||
| status
|
| status
|
||||||
|}
|
|}
|
||||||
@@ -82,14 +82,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation UpdateManga(
|
|mutation UpdateManga(
|
||||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|
||||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|
||||||
|) {
|
|) {
|
||||||
|SaveMediaListEntry(
|
|SaveMediaListEntry(
|
||||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
|id: $listId, progress: $progress, status: $status, private: $private,
|
||||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|
||||||
|) {
|
|) {
|
||||||
|id
|
|id
|
||||||
|status
|
|status
|
||||||
@@ -118,9 +118,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun deleteLibManga(track: DomainTrack) {
|
suspend fun deleteLibManga(track: DomainTrack) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation DeleteManga(${'$'}listId: Int) {
|
|mutation DeleteManga($listId: Int) {
|
||||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
|DeleteMediaListEntry(id: $listId) {
|
||||||
|deleted
|
|deleted
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
@@ -139,10 +139,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query Search(${'$'}query: String) {
|
|query Search($query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
|id
|
|id
|
||||||
|staff {
|
|staff {
|
||||||
|edges {
|
|edges {
|
||||||
@@ -201,10 +201,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query ($id: Int!, $manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|
||||||
|id
|
|id
|
||||||
|status
|
|status
|
||||||
|scoreRaw: score(format: POINT_100)
|
|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
|
// Users can set a 'username' (not nickname) once which effectively
|
||||||
// replaces the stringified ID in certain queries.
|
// replaces the stringified ID in certain queries.
|
||||||
// If no username is set, the API returns the user ID as a strings
|
// 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)
|
saveCredentials(username, oauth.accessToken)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authentication.apiUrl = prefApiUrl
|
authentication.apiUrl = prefApiUrl
|
||||||
authentication.jwtToken = token.toString()
|
authentication.jwtToken = token
|
||||||
}
|
}
|
||||||
authentications = oauth
|
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.MALOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
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.MALUser
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult
|
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.PkceUtil
|
import eu.kanade.tachiyomi.util.PkceUtil
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@@ -80,15 +77,15 @@ class MyAnimeListApi(
|
|||||||
// MAL API throws a 400 when the query is over 64 characters...
|
// MAL API throws a 400 when the query is over 64 characters...
|
||||||
.appendQueryParameter("q", query.take(64))
|
.appendQueryParameter("q", query.take(64))
|
||||||
.appendQueryParameter("nsfw", "true")
|
.appendQueryParameter("nsfw", "true")
|
||||||
|
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MALSearchResult>()
|
.parseAs<MALSearchResult>()
|
||||||
.data
|
.data
|
||||||
.map { async { getMangaDetails(it.node.id) } }
|
.filter { !(it.node.mediaType.contains("novel")) }
|
||||||
.awaitAll()
|
.map { parseSearchItem(it.node) }
|
||||||
.filter { !it.publishing_type.contains("novel") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,29 +94,13 @@ class MyAnimeListApi(
|
|||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||||
.appendPath(id.toString())
|
.appendPath(id.toString())
|
||||||
.appendQueryParameter(
|
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||||
"fields",
|
|
||||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MALManga>()
|
.parseAs<MALManga>()
|
||||||
.let {
|
.let { parseSearchItem(it) }
|
||||||
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 ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,8 +164,7 @@ class MyAnimeListApi(
|
|||||||
|
|
||||||
val matches = myListSearchResult.data
|
val matches = myListSearchResult.data
|
||||||
.filter { it.node.title.contains(query, ignoreCase = true) }
|
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||||
.map { async { getMangaDetails(it.node.id) } }
|
.map { parseSearchItem(it.node) }
|
||||||
.awaitAll()
|
|
||||||
|
|
||||||
// Check next page if there's more
|
// Check next page if there's more
|
||||||
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
||||||
@@ -216,12 +196,12 @@ class MyAnimeListApi(
|
|||||||
description = it.synopsis,
|
description = it.synopsis,
|
||||||
authors = it.authors
|
authors = it.authors
|
||||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
.mapNotNull { it.node.getFullName() }
|
||||||
.joinToString(separator = ", ")
|
.joinToString(separator = ", ")
|
||||||
.ifEmpty { null },
|
.ifEmpty { null },
|
||||||
artists = it.authors
|
artists = it.authors
|
||||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
.mapNotNull { it.node.getFullName() }
|
||||||
.joinToString(separator = ", ")
|
.joinToString(separator = ", ")
|
||||||
.ifEmpty { null },
|
.ifEmpty { null },
|
||||||
)
|
)
|
||||||
@@ -230,10 +210,10 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
private suspend fun getListPage(offset: Int): MALSearchResult {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
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())
|
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
urlBuilder.appendQueryParameter("offset", offset.toString())
|
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 {
|
private fun parseDate(isoDate: String): Long {
|
||||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,7 @@ class MyAnimeListApi(
|
|||||||
return try {
|
return try {
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
outputDf.format(epochTime)
|
outputDf.format(epochTime)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,6 +286,9 @@ class MyAnimeListApi(
|
|||||||
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
|
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 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 const val LIST_PAGINATION_AMOUNT = 250
|
||||||
|
|
||||||
private var codeVerifier: String = ""
|
private var codeVerifier: String = ""
|
||||||
|
|||||||
@@ -18,8 +18,26 @@ data class MALManga(
|
|||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
@SerialName("start_date")
|
@SerialName("start_date")
|
||||||
val startDate: String?,
|
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
|
@Serializable
|
||||||
data class MALMangaCovers(
|
data class MALMangaCovers(
|
||||||
val large: String = "",
|
val large: String = "",
|
||||||
@@ -33,19 +51,5 @@ data class MALMangaMetadata(
|
|||||||
val synopsis: String?,
|
val synopsis: String?,
|
||||||
@SerialName("main_picture")
|
@SerialName("main_picture")
|
||||||
val covers: MALMangaCovers,
|
val covers: MALMangaCovers,
|
||||||
val authors: List<MALAuthor>,
|
val authors: List<MALAuthorNode>,
|
||||||
)
|
|
||||||
|
|
||||||
@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,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResult(
|
data class MALSearchResult(
|
||||||
val data: List<MALSearchResultNode>,
|
val data: List<MALSearchResultNode>,
|
||||||
|
val paging: MALSearchPaging,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResultNode(
|
data class MALSearchResultNode(
|
||||||
val node: MALSearchResultItem,
|
val node: MALManga,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResultItem(
|
data class MALSearchPaging(
|
||||||
val id: Int,
|
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()
|
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
|
||||||
|
|
||||||
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query GetManga(${'$'}mangaId: Int!) {
|
|query GetManga($mangaId: Int!) {
|
||||||
| manga(id: ${'$'}mangaId) {
|
| manga(id: $mangaId) {
|
||||||
| ...MangaFragment
|
| ...MangaFragment
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
|
|
|
|
||||||
|$MangaFragment
|
|$$MangaFragment
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
val payload = buildJsonObject {
|
val payload = buildJsonObject {
|
||||||
put("query", query)
|
put("query", query)
|
||||||
@@ -87,9 +87,9 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
|
|
||||||
// TODO: Include a filter on the chapter number here
|
// 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
|
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
|
||||||
val chaptersQuery = """
|
val chaptersQuery = $$"""
|
||||||
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
|query GetMangaUnreadChapters($mangaId: Int!) {
|
||||||
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
|
||||||
| nodes {
|
| nodes {
|
||||||
| id
|
| id
|
||||||
| chapterNumber
|
| chapterNumber
|
||||||
@@ -115,24 +115,24 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
.data
|
.data
|
||||||
.entry
|
.entry
|
||||||
.nodes
|
.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) {
|
val markQuery = if (deleteDownloadsOnServer) {
|
||||||
"""
|
$$"""
|
||||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||||
| __typename
|
| __typename
|
||||||
| }
|
| }
|
||||||
| deleteDownloadedChapters(input: {ids: ${'$'}chapters}) {
|
| deleteDownloadedChapters(input: {ids: $chapters}) {
|
||||||
| __typename
|
| __typename
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
} else {
|
} else {
|
||||||
"""
|
$$"""
|
||||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||||
| __typename
|
| __typename
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
@@ -156,9 +156,9 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackQuery = """
|
val trackQuery = $$"""
|
||||||
|mutation TrackManga(${'$'}mangaId: Int!) {
|
|mutation TrackManga($mangaId: Int!) {
|
||||||
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
| trackProgress(input: {mangaId: $mangaId}) {
|
||||||
| __typename
|
| __typename
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
|
||||||
class PreferenceModule(val app: Application) : InjektModule {
|
class PreferenceModule(val app: Application) : InjektModule {
|
||||||
@@ -44,6 +45,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
LibraryPreferences(get())
|
LibraryPreferences(get())
|
||||||
}
|
}
|
||||||
|
addSingletonFactory {
|
||||||
|
UpdatesPreferences(get())
|
||||||
|
}
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
ReaderPreferences(get())
|
ReaderPreferences(get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
session.fsync(outputStream)
|
session.fsync(outputStream)
|
||||||
}
|
}
|
||||||
|
service.contentResolver.delete(entry.uri, null, null)
|
||||||
|
|
||||||
val intentSender = PendingIntent.getBroadcast(
|
val intentSender = PendingIntent.getBroadcast(
|
||||||
service,
|
service,
|
||||||
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||||
).intentSender
|
).intentSender
|
||||||
|
@SuppressLint("RequestInstallPackagesPolicy")
|
||||||
session.commit(intentSender)
|
session.commit(intentSender)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun processEntry(entry: Entry) {
|
override fun processEntry(entry: Entry) {
|
||||||
super.processEntry(entry)
|
super.processEntry(entry)
|
||||||
try {
|
try {
|
||||||
shellInterface?.install(
|
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
|
||||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
shellInterface?.install(it)
|
||||||
)
|
}
|
||||||
|
service.contentResolver.delete(entry.uri, null, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
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)
|
service.unregisterReceiver(receiver)
|
||||||
logcat { "ShizukuInstaller destroy" }
|
logcat { "ShizukuInstaller destroy" }
|
||||||
scope.cancel()
|
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) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager = Injekt.get<ExtensionManager>()
|
||||||
|
|||||||
@@ -1,66 +1,48 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
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.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
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.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
internal class ExtensionInstaller(private val context: 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>>()
|
|
||||||
|
|
||||||
|
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 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
|
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||||
* step in the installation process.
|
* step in the installation process.
|
||||||
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param extension The extension to install.
|
* @param extension The extension to install.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
|
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]
|
val step = MutableStateFlow(InstallStep.Pending)
|
||||||
if (oldDownload != null) {
|
activeSteps[downloadId] = step
|
||||||
deleteDownload(pkgName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the receiver after removing (and unregistering) the previous download
|
val job = scope.launch {
|
||||||
downloadReceiver.register()
|
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()
|
if (!response.isSuccessful) {
|
||||||
val request = DownloadManager.Request(downloadUri)
|
throw Exception("Failed to download extension")
|
||||||
.setTitle(extension.name)
|
}
|
||||||
.setMimeType(APK_MIME)
|
response.body.byteStream().use { input ->
|
||||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
tmpFile.outputStream().use { output ->
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val id = downloadManager.enqueue(request)
|
step.value = InstallStep.Installing
|
||||||
activeDownloads[pkgName] = id
|
installApk(downloadId, tmpFile)
|
||||||
|
} catch (e: Exception) {
|
||||||
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
if (e is InterruptedException) {
|
||||||
downloadsStateFlows[id] = downloadStateFlow
|
// Canceled
|
||||||
|
} else {
|
||||||
// Poll download status
|
logcat(LogPriority.ERROR, e)
|
||||||
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
|
step.value = InstallStep.Error
|
||||||
// Map to our model
|
}
|
||||||
when (downloadStatus) {
|
|
||||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
|
||||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
|
activeJobs[extension.pkgName] = job
|
||||||
emit(it)
|
|
||||||
// Stop when the application is installed or errors
|
return step.asStateFlow()
|
||||||
!it.isCompleted()
|
.onCompletion {
|
||||||
}.onCompletion {
|
activeJobs.remove(extension.pkgName)
|
||||||
// Always notify on main thread
|
activeSteps.remove(downloadId)
|
||||||
withUIContext {
|
job.cancel()
|
||||||
// Always remove the download when unsubscribed
|
|
||||||
deleteDownload(pkgName)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* 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()) {
|
when (val installer = extensionInstaller.get()) {
|
||||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||||
.setDataAndType(uri, APK_MIME)
|
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
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 {
|
try {
|
||||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
|
||||||
tempFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
updateInstallStep(downloadId, InstallStep.Installed)
|
||||||
} else {
|
} else {
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
updateInstallStep(downloadId, InstallStep.Error)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
updateInstallStep(downloadId, InstallStep.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
val intent = ExtensionInstallService.getIntent(
|
||||||
|
context,
|
||||||
|
downloadId,
|
||||||
|
tempFile.getUriCompat(context),
|
||||||
|
installer,
|
||||||
|
)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
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.
|
* Cancels extension install and remove from download manager and installer.
|
||||||
*/
|
*/
|
||||||
fun cancelInstall(pkgName: String) {
|
fun cancelInstall(pkgName: String) {
|
||||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
activeJobs.remove(pkgName)?.cancel()
|
||||||
downloadManager.remove(downloadId)
|
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
|
||||||
Installer.cancelInstallQueue(context, downloadId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param step New install step.
|
* @param step New install step.
|
||||||
*/
|
*/
|
||||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||||
downloadsStateFlows[downloadId]?.let { it.value = step }
|
activeSteps[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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val APK_MIME = "application/vnd.android.package-archive"
|
const val APK_MIME = "application/vnd.android.package-archive"
|
||||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
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)
|
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 {
|
screenModelScope.launchIO {
|
||||||
combine(
|
combine(
|
||||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
state.map { it.searchQuery }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.debounce(SEARCH_DEBOUNCE_MILLIS)
|
||||||
|
.map { searchQueryPredicate(it ?: "") },
|
||||||
currentDownloads,
|
currentDownloads,
|
||||||
getExtensions.subscribe(),
|
getExtensions.subscribe(),
|
||||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||||
val searchQuery = query ?: ""
|
buildMap {
|
||||||
|
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||||
val itemsGroups: ItemGroups = mutableMapOf()
|
if (updates.isNotEmpty()) {
|
||||||
|
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
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 ->
|
mutableState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = it,
|
items = items,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
|
|||||||
.launchIn(screenModelScope)
|
.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?) {
|
fun search(query: String?) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(searchQuery = query)
|
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 {
|
object ExtensionUiModel {
|
||||||
sealed interface Header {
|
sealed interface Header {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -49,6 +50,10 @@ fun extensionsTab(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
content = { contentPadding, _ ->
|
content = { contentPadding, _ ->
|
||||||
|
BackHandler(enabled = state.searchQuery != null) {
|
||||||
|
extensionsScreenModel.search(null)
|
||||||
|
}
|
||||||
|
|
||||||
ExtensionScreen(
|
ExtensionScreen(
|
||||||
state = state,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
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.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (state.selectionMode) {
|
SmallExtendedFloatingActionButton(
|
||||||
ExtendedFloatingActionButton(
|
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
icon = {
|
||||||
icon = {
|
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
},
|
||||||
},
|
onClick = {
|
||||||
onClick = {
|
val selection = state.selection
|
||||||
val selection = state.selection
|
screenModel.clearSelection()
|
||||||
screenModel.clearSelection()
|
navigator.push(MigrationConfigScreen(selection))
|
||||||
navigator.push(MigrationConfigScreen(selection))
|
},
|
||||||
},
|
expanded = lazyListState.shouldExpandFAB(),
|
||||||
expanded = lazyListState.shouldExpandFAB(),
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
)
|
visible = state.selectionMode,
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
|
|||||||
+13
-9
@@ -1,17 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
@@ -35,7 +38,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
|
|||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
@@ -74,13 +76,15 @@ data class MigrateSourceSearchScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
|
SmallExtendedFloatingActionButton(
|
||||||
ExtendedFloatingActionButton(
|
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
onClick = screenModel::openFilterSheet,
|
||||||
onClick = screenModel::openFilterSheet,
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
)
|
visible = state.filters.isNotEmpty(),
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
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.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
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.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import tachiyomi.core.common.util.lang.launchUI
|
import tachiyomi.core.common.util.lang.launchUI
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.Pill
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||||
visible = downloadList.isNotEmpty(),
|
SmallExtendedFloatingActionButton(
|
||||||
enter = fadeIn(),
|
text = {
|
||||||
exit = fadeOut(),
|
val id = if (isRunning) {
|
||||||
) {
|
MR.strings.action_pause
|
||||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
} else {
|
||||||
ExtendedFloatingActionButton(
|
MR.strings.action_resume
|
||||||
text = {
|
}
|
||||||
val id = if (isRunning) {
|
Text(text = stringResource(id))
|
||||||
MR.strings.action_pause
|
},
|
||||||
} else {
|
icon = {
|
||||||
MR.strings.action_resume
|
val icon = if (isRunning) {
|
||||||
}
|
Icons.Outlined.Pause
|
||||||
Text(text = stringResource(id))
|
} else {
|
||||||
},
|
Icons.Filled.PlayArrow
|
||||||
icon = {
|
}
|
||||||
val icon = if (isRunning) {
|
Icon(imageVector = icon, contentDescription = null)
|
||||||
Icons.Outlined.Pause
|
},
|
||||||
} else {
|
onClick = {
|
||||||
Icons.Filled.PlayArrow
|
if (isRunning) {
|
||||||
}
|
screenModel.pauseDownloads()
|
||||||
Icon(imageVector = icon, contentDescription = null)
|
} else {
|
||||||
},
|
screenModel.startDownloads()
|
||||||
onClick = {
|
}
|
||||||
if (isRunning) {
|
},
|
||||||
screenModel.pauseDownloads()
|
expanded = fabExpanded,
|
||||||
} else {
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
screenModel.startDownloads()
|
visible = downloadList.isNotEmpty(),
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
},
|
),
|
||||||
expanded = fabExpanded,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (downloadList.isEmpty()) {
|
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.GetCategories
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
@@ -122,6 +123,7 @@ class LibraryScreenModel(
|
|||||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||||
|
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
|
||||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
@@ -404,9 +406,7 @@ class LibraryScreenModel(
|
|||||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||||
|
|
||||||
val mangaTracks = trackMap
|
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
|
||||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
|
||||||
.orEmpty()
|
|
||||||
|
|
||||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
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
|
* Queues the amount specified of unread chapters from the list of selected manga
|
||||||
*/
|
*/
|
||||||
fun performDownloadAction(action: DownloadAction) {
|
fun performDownloadAction(action: DownloadAction) {
|
||||||
val mangas = state.value.selectedManga
|
when (action) {
|
||||||
val amount = when (action) {
|
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
|
||||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
|
||||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
|
||||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
|
||||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
|
||||||
DownloadAction.UNREAD_CHAPTERS -> null
|
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
|
||||||
}
|
}
|
||||||
clearSelection()
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadNextChapters(amount: Int?) {
|
||||||
|
val mangas = state.value.selectedManga
|
||||||
screenModelScope.launchNonCancellable {
|
screenModelScope.launchNonCancellable {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
// SY -->
|
// 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 -->
|
// SY -->
|
||||||
fun cleanTitles() {
|
fun cleanTitles() {
|
||||||
state.value.selectedManga.fastFilter {
|
state.value.selectedManga.fastFilter {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
@@ -208,7 +207,9 @@ class MangaScreen(
|
|||||||
previewsRowCount = successState.previewsRowCount,
|
previewsRowCount = successState.previewsRowCount,
|
||||||
onMigrateClicked = {
|
onMigrateClicked = {
|
||||||
navigator.push(MigrationConfigScreen(successState.manga.id))
|
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)) },
|
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||||
@@ -403,12 +404,7 @@ class MangaScreen(
|
|||||||
try {
|
try {
|
||||||
getMangaUrl(manga_, source_)?.let { url ->
|
getMangaUrl(manga_, source_)?.let { url ->
|
||||||
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
||||||
context.startActivity(
|
context.startActivity(intent)
|
||||||
Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
context.stringResource(MR.strings.action_share),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toast(e.message)
|
context.toast(e.message)
|
||||||
|
|||||||
@@ -1175,6 +1175,13 @@ class MangaScreenModel(
|
|||||||
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
|
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(
|
private fun startDownload(
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
startNow: Boolean,
|
startNow: Boolean,
|
||||||
@@ -1237,6 +1244,7 @@ class MangaScreenModel(
|
|||||||
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
||||||
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
||||||
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
||||||
|
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
|
||||||
}
|
}
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
startDownload(chaptersToDownload, false)
|
startDownload(chaptersToDownload, false)
|
||||||
@@ -1487,7 +1495,6 @@ class MangaScreenModel(
|
|||||||
fun toggleSelection(
|
fun toggleSelection(
|
||||||
item: ChapterList.Item,
|
item: ChapterList.Item,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
userSelected: Boolean = false,
|
|
||||||
fromLongPress: Boolean = false,
|
fromLongPress: Boolean = false,
|
||||||
) {
|
) {
|
||||||
updateSuccessState { successState ->
|
updateSuccessState { successState ->
|
||||||
@@ -1502,7 +1509,7 @@ class MangaScreenModel(
|
|||||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
selectedChapterIds.addOrRemove(item.id, selected)
|
selectedChapterIds.addOrRemove(item.id, selected)
|
||||||
|
|
||||||
if (selected && userSelected && fromLongPress) {
|
if (selected && fromLongPress) {
|
||||||
if (firstSelection) {
|
if (firstSelection) {
|
||||||
selectedPositions[0] = selectedIndex
|
selectedPositions[0] = selectedIndex
|
||||||
selectedPositions[1] = selectedIndex
|
selectedPositions[1] = selectedIndex
|
||||||
@@ -1528,7 +1535,7 @@ class MangaScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (userSelected && !fromLongPress) {
|
} else if (!fromLongPress) {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
if (selectedIndex == selectedPositions[0]) {
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
selectedPositions[0] = indexOfFirst { it.selected }
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
|
|||||||
@@ -934,7 +934,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
private fun shareChapter() {
|
private fun shareChapter() {
|
||||||
assistUrl?.let {
|
assistUrl?.let {
|
||||||
val intent = it.toUri().toShareIntent(this, type = "text/plain")
|
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,
|
context = applicationContext,
|
||||||
message = /* SY --> */ text, // SY <--
|
message = /* SY --> */ text, // SY <--
|
||||||
)
|
)
|
||||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCopyImageResult(uri: Uri) {
|
private fun onCopyImageResult(uri: Uri) {
|
||||||
|
|||||||
@@ -655,7 +655,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
* if setting is enabled and [currentChapter] is queued for download
|
* if setting is enabled and [currentChapter] is queued for download
|
||||||
*/
|
*/
|
||||||
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): 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))
|
downloadManager.cancelQueuedDownloads(listOf(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -848,7 +848,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
updateChapter.await(
|
updateChapter.await(
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
id = chapter.id!!.toLong(),
|
id = chapter.id!!,
|
||||||
bookmark = bookmarked,
|
bookmark = bookmarked,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,15 +69,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition
|
||||||
if (chapters.prevChapter != null) {
|
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
// 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) {
|
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) {
|
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resets double-page splits, else insert pages get misplaced
|
// Resets double-page splits, else insert pages get misplaced
|
||||||
subItems.filterIsInstance<InsertPage>().also { subItems.removeAll(it) }
|
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
|
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition.
|
||||||
if (chapters.prevChapter != null) {
|
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
// 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) {
|
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))
|
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chapters.nextChapter != null) {
|
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItems(newItems)
|
updateItems(newItems)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.util.fastFilter
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
@@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
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.merge
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.common.preference.TriState
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.common.util.system.logcat
|
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.chapter.model.ChapterUpdate
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||||
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
@@ -60,6 +68,7 @@ class UpdatesScreenModel(
|
|||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
|
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
// SY -->
|
// SY -->
|
||||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||||
@@ -85,19 +94,35 @@ class UpdatesScreenModel(
|
|||||||
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
||||||
|
|
||||||
combine(
|
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,
|
downloadCache.changes,
|
||||||
downloadManager.queueState,
|
downloadManager.queueState,
|
||||||
) { updates, _, _ -> updates }
|
// needed for Kotlin filters (downloaded)
|
||||||
.catch {
|
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
|
||||||
logcat(LogPriority.ERROR, it)
|
old.filterDownloaded == new.filterDownloaded
|
||||||
_events.send(Event.InternalError)
|
},
|
||||||
}
|
) { updates, _, _, itemPreferences ->
|
||||||
.collectLatest { updates ->
|
updates
|
||||||
|
.toUpdateItems()
|
||||||
|
.applyFilters(itemPreferences)
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
|
.collectLatest { updateItems ->
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = updates.toUpdateItems(),
|
items = updateItems,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,9 +133,43 @@ class UpdatesScreenModel(
|
|||||||
.catch { logcat(LogPriority.ERROR, it) }
|
.catch { logcat(LogPriority.ERROR, it) }
|
||||||
.collect(this@UpdatesScreenModel::updateDownloadState)
|
.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
|
return this
|
||||||
.map { update ->
|
.map { update ->
|
||||||
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
||||||
@@ -135,7 +194,6 @@ class UpdatesScreenModel(
|
|||||||
selected = update.chapterId in selectedChapterIds,
|
selected = update.chapterId in selectedChapterIds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toPersistentList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibrary(): Boolean {
|
fun updateLibrary(): Boolean {
|
||||||
@@ -279,7 +337,6 @@ class UpdatesScreenModel(
|
|||||||
fun toggleSelection(
|
fun toggleSelection(
|
||||||
item: UpdatesItem,
|
item: UpdatesItem,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
userSelected: Boolean = false,
|
|
||||||
fromLongPress: Boolean = false,
|
fromLongPress: Boolean = false,
|
||||||
) {
|
) {
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
@@ -294,7 +351,7 @@ class UpdatesScreenModel(
|
|||||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||||
|
|
||||||
if (selected && userSelected && fromLongPress) {
|
if (selected && fromLongPress) {
|
||||||
if (firstSelection) {
|
if (firstSelection) {
|
||||||
selectedPositions[0] = selectedIndex
|
selectedPositions[0] = selectedIndex
|
||||||
selectedPositions[1] = selectedIndex
|
selectedPositions[1] = selectedIndex
|
||||||
@@ -320,7 +377,7 @@ class UpdatesScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (userSelected && !fromLongPress) {
|
} else if (!fromLongPress) {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
if (selectedIndex == selectedPositions[0]) {
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
selectedPositions[0] = indexOfFirst { it.selected }
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
@@ -373,9 +430,41 @@ class UpdatesScreenModel(
|
|||||||
libraryPreferences.newUpdatesCount().set(0)
|
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
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
val hasActiveFilters: Boolean = false,
|
||||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||||
val dialog: Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
) {
|
) {
|
||||||
@@ -399,6 +488,7 @@ class UpdatesScreenModel(
|
|||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
||||||
|
data object FilterSheet : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Event {
|
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
|
@Immutable
|
||||||
data class UpdatesItem(
|
data class UpdatesItem(
|
||||||
val update: UpdatesWithRelations,
|
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.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.updates.UpdateScreen
|
import eu.kanade.presentation.updates.UpdateScreen
|
||||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||||
|
import eu.kanade.presentation.updates.UpdatesFilterDialog
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||||
@@ -70,6 +71,7 @@ data object UpdatesTab : Tab {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||||
|
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
UpdateScreen(
|
UpdateScreen(
|
||||||
@@ -93,6 +95,8 @@ data object UpdatesTab : Tab {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||||
|
onFilterClicked = screenModel::showFilterDialog,
|
||||||
|
hasActiveFilters = state.hasActiveFilters,
|
||||||
)
|
)
|
||||||
|
|
||||||
val onDismissDialog = { screenModel.setDialog(null) }
|
val onDismissDialog = { screenModel.setDialog(null) }
|
||||||
@@ -103,6 +107,12 @@ data object UpdatesTab : Tab {
|
|||||||
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is UpdatesScreenModel.Dialog.FilterSheet -> {
|
||||||
|
UpdatesFilterDialog(
|
||||||
|
onDismissRequest = onDismissDialog,
|
||||||
|
screenModel = settingsScreenModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CrashLogUtil(
|
|||||||
|
|
||||||
suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext {
|
suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext {
|
||||||
try {
|
try {
|
||||||
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
|
val file = context.createFileInCacheDir("tachiyomi_sy_crash_logs.txt")
|
||||||
|
|
||||||
file.appendText(getDebugInfo() + "\n\n")
|
file.appendText(getDebugInfo() + "\n\n")
|
||||||
getExtensionsInfo()?.let { file.appendText("$it\n\n") }
|
getExtensionsInfo()?.let { file.appendText("$it\n\n") }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package androidx.preference
|
|||||||
/**
|
/**
|
||||||
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||||
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||||
return onBindEditTextListener
|
return onBindEditTextListener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package exh.ui.login
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
@@ -13,6 +12,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.presentation.webview.EhLoginWebViewScreen
|
import eu.kanade.presentation.webview.EhLoginWebViewScreen
|
||||||
import eu.kanade.presentation.webview.components.IgneousDialog
|
import eu.kanade.presentation.webview.components.IgneousDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@@ -92,16 +92,32 @@ class EhLoginActivity : BaseActivity() {
|
|||||||
|
|
||||||
private fun onPageFinished(view: WebView, url: String, customIgneous: String?) {
|
private fun onPageFinished(view: WebView, url: String, customIgneous: String?) {
|
||||||
xLogD(url)
|
xLogD(url)
|
||||||
val parsedUrl = Uri.parse(url)
|
val parsedUrl = url.toUri()
|
||||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||||
// Hide distracting content
|
view.evaluateJavascript(
|
||||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
"""
|
||||||
view.evaluateJavascript(HIDE_JS, null)
|
(function() {
|
||||||
}
|
let html = document.documentElement.innerHTML;
|
||||||
// Check login result
|
return html.includes("/cdn-cgi/");
|
||||||
|
})();
|
||||||
|
""".trimIndent()
|
||||||
|
) { result ->
|
||||||
|
val isCloudflareBlock = result == "true"
|
||||||
|
|
||||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
if (isCloudflareBlock) {
|
||||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
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)) {
|
} else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
|
||||||
// At ExHentai, check that everything worked out...
|
// At ExHentai, check that everything worked out...
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -62,7 +63,6 @@ import tachiyomi.domain.source.service.SourceManager
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.Pill
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -144,7 +144,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
SmallExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||||
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
||||||
onClick = {
|
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 ->
|
mutableState.update { state ->
|
||||||
val updatedSources = action(state.sources)
|
val updatedSources = action(state.sources)
|
||||||
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
||||||
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
||||||
}
|
}
|
||||||
if (save) saveSources()
|
saveSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSources() {
|
private fun initSources() {
|
||||||
@@ -370,7 +370,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
|||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
updateSources(save = false) { sources }
|
mutableState.update { state ->
|
||||||
|
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSelection(id: Long) {
|
fun toggleSelection(id: Long) {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
|
|||||||
}
|
}
|
||||||
val selectedFlags = sourcePreference.migrationFlags().get()
|
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(
|
State(
|
||||||
current = current,
|
current = current,
|
||||||
target = target,
|
target = target,
|
||||||
applicableFlags = applicableFlags,
|
applicableFlags = applicableFlags,
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ fun CalenderHeader(
|
|||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
IconButton(onClick = onPreviousClick) {
|
IconButton(onClick = onPreviousClick) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
||||||
}
|
}
|
||||||
IconButton(onClick = onNextClick) {
|
IconButton(onClick = onNextClick) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
|
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.the
|
||||||
import org.gradle.kotlin.dsl.withType
|
import org.gradle.kotlin.dsl.withType
|
||||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
|
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
|
||||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *,
|
|||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(AndroidConfig.JvmTarget)
|
jvmTarget.set(AndroidConfig.JvmTarget)
|
||||||
freeCompilerArgs.addAll(
|
freeCompilerArgs.addAll(
|
||||||
"-Xcontext-receivers",
|
"-Xcontext-parameters",
|
||||||
"-opt-in=kotlin.RequiresOptIn",
|
"-opt-in=kotlin.RequiresOptIn",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,8 +72,6 @@ internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *,
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensions.configure<ComposeCompilerGradlePluginExtension> {
|
extensions.configure<ComposeCompilerGradlePluginExtension> {
|
||||||
featureFlags.set(setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups))
|
|
||||||
|
|
||||||
val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean()
|
val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean()
|
||||||
val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").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)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(Json)
|
context(_: Json)
|
||||||
inline fun <reified T> Response.parseAs(): T {
|
inline fun <reified T> Response.parseAs(): T {
|
||||||
return decodeFromJsonResponse(serializer(), this)
|
return decodeFromJsonResponse(serializer(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(Json)
|
context(json: Json)
|
||||||
fun <T> decodeFromJsonResponse(
|
fun <T> decodeFromJsonResponse(
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>,
|
||||||
response: Response,
|
response: Response,
|
||||||
): T {
|
): T {
|
||||||
return response.body.source().use {
|
return response.body.source().use {
|
||||||
decodeFromBufferedSource(deserializer, it)
|
json.decodeFromBufferedSource(deserializer, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -73,7 +73,7 @@ class CloudflareInterceptor(
|
|||||||
executor.execute {
|
executor.execute {
|
||||||
webview = createWebView(originalRequest)
|
webview = createWebView(originalRequest)
|
||||||
|
|
||||||
webview?.webViewClient = object : WebViewClientCompat() {
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
fun isCloudFlareBypassed(): Boolean {
|
fun isCloudFlareBypassed(): Boolean {
|
||||||
return cookieManager.get(origRequestUrl.toHttpUrl())
|
return cookieManager.get(origRequestUrl.toHttpUrl())
|
||||||
@@ -111,7 +111,7 @@ class CloudflareInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webview?.loadUrl(origRequestUrl, headers)
|
webview.loadUrl(origRequestUrl, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
latch.awaitFor30Seconds()
|
latch.awaitFor30Seconds()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.util.system
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.webkit.WebResourceError
|
import android.webkit.WebResourceError
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
abstract class WebViewClientCompat : WebViewClient() {
|
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(
|
final override fun shouldOverrideUrlLoading(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
request: WebResourceRequest,
|
request: WebResourceRequest,
|
||||||
@@ -36,6 +36,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
|||||||
return shouldOverrideUrlCompat(view, request.url.toString())
|
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("shouldOverrideUrlLoading(WebView, WebResourceRequest)")
|
||||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||||
return shouldOverrideUrlCompat(view, url)
|
return shouldOverrideUrlCompat(view, url)
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
|||||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("shouldInterceptRequest(WebView, WebResourceRequest)")
|
||||||
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
||||||
return shouldInterceptRequestCompat(view, url)
|
return shouldInterceptRequestCompat(view, url)
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("onReceivedError(WebView, WebResourceRequest, WebResourceError)")
|
||||||
final override fun onReceivedError(
|
final override fun onReceivedError(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
errorCode: Int,
|
errorCode: Int,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import me.zhanghai.android.libarchive.ArchiveException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
import mihon.core.common.archive.ArchiveEntry as MihonArchiveEntry
|
||||||
|
|
||||||
class ArchiveInputStream(
|
class ArchiveInputStream(
|
||||||
buffer: Long,
|
buffer: Long,
|
||||||
@@ -67,18 +68,20 @@ class ArchiveInputStream(
|
|||||||
Archive.readFree(archive)
|
Archive.readFree(archive)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry ->
|
fun getNextEntry(): MihonArchiveEntry? {
|
||||||
val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null
|
return Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry ->
|
||||||
val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG
|
val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null
|
||||||
// SY -->
|
val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG
|
||||||
val isEncrypted = ArchiveEntry.isEncrypted(entry)
|
|
||||||
// SY <--
|
|
||||||
ArchiveEntry(
|
|
||||||
name,
|
|
||||||
isFile,
|
|
||||||
// SY -->
|
// SY -->
|
||||||
isEncrypted,
|
val isEncrypted = ArchiveEntry.isEncrypted(entry)
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
MihonArchiveEntry(
|
||||||
|
name,
|
||||||
|
isFile,
|
||||||
|
// SY -->
|
||||||
|
isEncrypted,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,21 @@ class AndroidDatabaseHandler(
|
|||||||
// SY -->
|
// SY -->
|
||||||
fun getLibraryQuery(condition: String = "M.favorite = 1") = LibraryQuery(driver, condition)
|
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 <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,72 +24,113 @@ private val mapper = { cursor: SqlCursor ->
|
|||||||
cursor.getLong(12)!!,
|
cursor.getLong(12)!!,
|
||||||
cursor.getLong(13)!!,
|
cursor.getLong(13)!!,
|
||||||
cursor.getLong(14)!!,
|
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> {
|
override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> {
|
||||||
return driver.executeQuery(
|
return driver.executeQuery(
|
||||||
null,
|
null,
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT *
|
||||||
mangas._id AS mangaId,
|
FROM (
|
||||||
mangas.title AS mangaTitle,
|
-- Normal source
|
||||||
chapters._id AS chapterId,
|
SELECT
|
||||||
chapters.name AS chapterName,
|
mangas._id AS mangaId,
|
||||||
chapters.scanlator,
|
mangas.title AS mangaTitle,
|
||||||
chapters.url AS chapterUrl,
|
chapters._id AS chapterId,
|
||||||
chapters.read,
|
chapters.name AS chapterName,
|
||||||
chapters.bookmark,
|
chapters.scanlator,
|
||||||
chapters.last_page_read,
|
chapters.url AS chapterUrl,
|
||||||
mangas.source,
|
chapters.read,
|
||||||
mangas.favorite,
|
chapters.bookmark,
|
||||||
mangas.thumbnail_url AS thumbnailUrl,
|
chapters.last_page_read,
|
||||||
mangas.cover_last_modified AS coverLastModified,
|
mangas.source,
|
||||||
chapters.date_upload AS dateUpload,
|
mangas.favorite,
|
||||||
chapters.date_fetch AS datefetch
|
mangas.thumbnail_url AS thumbnailUrl,
|
||||||
FROM mangas JOIN chapters
|
mangas.cover_last_modified AS coverLastModified,
|
||||||
ON mangas._id = chapters.manga_id
|
chapters.date_upload AS dateUpload,
|
||||||
WHERE favorite = 1 AND source <> $MERGED_SOURCE_ID
|
chapters.date_fetch AS datefetch,
|
||||||
AND date_fetch > date_added
|
excluded_scanlators.scanlator AS excludedScanlator
|
||||||
AND dateUpload > :after
|
FROM mangas
|
||||||
UNION
|
JOIN chapters
|
||||||
SELECT
|
ON mangas._id = chapters.manga_id
|
||||||
mangas._id AS mangaId,
|
LEFT JOIN excluded_scanlators
|
||||||
mangas.title AS mangaTitle,
|
ON mangas._id = excluded_scanlators.manga_id
|
||||||
chapters._id AS chapterId,
|
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||||
chapters.name AS chapterName,
|
WHERE mangas.source <> $MERGED_SOURCE_ID
|
||||||
chapters.scanlator,
|
AND date_fetch > date_added
|
||||||
chapters.url AS chapterUrl,
|
|
||||||
chapters.read,
|
UNION ALL
|
||||||
chapters.bookmark,
|
|
||||||
chapters.last_page_read,
|
-- Merged source
|
||||||
mangas.source,
|
SELECT
|
||||||
mangas.favorite,
|
mangas._id AS mangaId,
|
||||||
mangas.thumbnail_url AS thumbnailUrl,
|
mangas.title AS mangaTitle,
|
||||||
mangas.cover_last_modified AS coverLastModified,
|
chapters._id AS chapterId,
|
||||||
chapters.date_upload AS dateUpload,
|
chapters.name AS chapterName,
|
||||||
chapters.date_fetch AS datefetch
|
chapters.scanlator,
|
||||||
FROM mangas
|
chapters.url AS chapterUrl,
|
||||||
LEFT JOIN (
|
chapters.read,
|
||||||
SELECT merged.manga_id,merged.merge_id
|
chapters.bookmark,
|
||||||
FROM merged
|
chapters.last_page_read,
|
||||||
GROUP BY merged.merge_id
|
mangas.source,
|
||||||
) as ME
|
mangas.favorite,
|
||||||
ON ME.merge_id = mangas._id
|
mangas.thumbnail_url AS thumbnailUrl,
|
||||||
JOIN chapters
|
mangas.cover_last_modified AS coverLastModified,
|
||||||
ON ME.manga_id = chapters.manga_id
|
chapters.date_upload AS dateUpload,
|
||||||
WHERE favorite = 1 AND source = $MERGED_SOURCE_ID
|
chapters.date_fetch AS datefetch,
|
||||||
AND date_fetch > date_added
|
excluded_scanlators.scanlator AS excludedScanlator
|
||||||
AND dateUpload > :after
|
FROM mangas
|
||||||
ORDER BY datefetch DESC
|
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;
|
LIMIT :limit;
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
mapper,
|
mapper,
|
||||||
2,
|
6,
|
||||||
binders = {
|
binders = {
|
||||||
bindLong(0, after)
|
var parameterIndex = 0
|
||||||
bindLong(1, limit)
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import tachiyomi.core.common.util.lang.toLong
|
||||||
import tachiyomi.data.AndroidDatabaseHandler
|
import tachiyomi.data.AndroidDatabaseHandler
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
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 {
|
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 {
|
}.map {
|
||||||
databaseHandler.awaitListExecutable {
|
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)
|
.map(::mapUpdatesView)
|
||||||
}
|
}
|
||||||
@@ -70,6 +95,7 @@ class UpdatesRepositoryImpl(
|
|||||||
coverLastModified: Long,
|
coverLastModified: Long,
|
||||||
dateUpload: Long,
|
dateUpload: Long,
|
||||||
dateFetch: Long,
|
dateFetch: Long,
|
||||||
|
excludedScanlator: String?,
|
||||||
): UpdatesWithRelations = UpdatesWithRelations(
|
): UpdatesWithRelations = UpdatesWithRelations(
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ CREATE TABLE chapters(
|
|||||||
|
|
||||||
CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
|
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 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
|
CREATE TRIGGER update_last_modified_at_chapters
|
||||||
AFTER UPDATE ON 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 excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id);
|
||||||
|
CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator);
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT INTO excluded_scanlators(manga_id, scanlator)
|
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 history_history_chapter_id_index ON history(chapter_id);
|
||||||
|
CREATE INDEX idx_history_last_read ON history(last_read);
|
||||||
|
|
||||||
getHistoryByMangaId:
|
getHistoryByMangaId:
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ CREATE TABLE manga_sync(
|
|||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id);
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
DELETE FROM manga_sync
|
DELETE FROM manga_sync
|
||||||
WHERE manga_id = :mangaId AND sync_id = :syncId;
|
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 library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||||
CREATE INDEX mangas_url_index ON mangas(url);
|
CREATE INDEX mangas_url_index ON mangas(url);
|
||||||
|
CREATE INDEX idx_mangas_source ON mangas(source);
|
||||||
|
|
||||||
CREATE TRIGGER update_last_favorited_at_mangas
|
CREATE TRIGGER update_last_favorited_at_mangas
|
||||||
AFTER UPDATE OF favorite ON mangas
|
AFTER UPDATE OF favorite ON mangas
|
||||||
@@ -118,12 +119,23 @@ AND source = :sourceId;
|
|||||||
|
|
||||||
getDuplicateLibraryManga:
|
getDuplicateLibraryManga:
|
||||||
WITH
|
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 (
|
duplicates AS (
|
||||||
SELECT *
|
SELECT M.*
|
||||||
FROM mangas
|
FROM mangas M
|
||||||
|
LEFT JOIN track_dupes D
|
||||||
|
ON D.manga_id = _id
|
||||||
WHERE favorite = 1
|
WHERE favorite = 1
|
||||||
AND _id != :id
|
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 (
|
chapter_counts AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ CREATE TABLE mangas_categories(
|
|||||||
ON DELETE CASCADE
|
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
|
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE mangas
|
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.thumbnail_url AS thumbnailUrl,
|
||||||
mangas.cover_last_modified AS coverLastModified,
|
mangas.cover_last_modified AS coverLastModified,
|
||||||
chapters.date_upload AS dateUpload,
|
chapters.date_upload AS dateUpload,
|
||||||
chapters.date_fetch AS datefetch
|
chapters.date_fetch AS datefetch,
|
||||||
|
excluded_scanlators.scanlator AS excludedScanlator
|
||||||
FROM mangas JOIN chapters
|
FROM mangas JOIN chapters
|
||||||
ON mangas._id = chapters.manga_id
|
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
|
WHERE favorite = 1
|
||||||
AND date_fetch > date_added
|
AND date_fetch > date_added
|
||||||
ORDER BY date_fetch DESC;
|
ORDER BY date_fetch DESC;
|
||||||
@@ -27,6 +31,23 @@ FROM updatesView
|
|||||||
WHERE dateUpload > :after
|
WHERE dateUpload > :after
|
||||||
LIMIT :limit;
|
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:
|
getUpdatesByReadStatus:
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM updatesView
|
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 <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(instant: Instant): Flow<List<UpdatesWithRelations>> {
|
fun subscribe(
|
||||||
return repository.subscribeAll(instant.toEpochMilli(), limit = 500)
|
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 -->
|
// SY -->
|
||||||
.catchNPE()
|
.catchNPE()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ interface UpdatesRepository {
|
|||||||
|
|
||||||
suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations>
|
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>>
|
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]
|
[versions]
|
||||||
agp_version = "8.13.2"
|
agp_version = "8.13.2"
|
||||||
lifecycle_version = "2.10.0"
|
lifecycle_version = "2.10.0"
|
||||||
paging_version = "3.3.6"
|
paging_version = "3.4.1"
|
||||||
interpolator_version = "1.0.0"
|
interpolator_version = "1.0.0"
|
||||||
|
|
||||||
[libraries]
|
[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-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }
|
||||||
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", 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-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
compose-bom = "2025.12.01"
|
compose-bom = "2026.02.00"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||||
animation = { module = "androidx.compose.animation:animation" }
|
animation = { module = "androidx.compose.animation:animation" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "2.3.0"
|
kotlin_version = "2.3.10"
|
||||||
serialization_version = "1.9.0"
|
serialization_version = "1.10.0"
|
||||||
xml_serialization_version = "0.91.3"
|
xml_serialization_version = "0.91.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "13.2.1"
|
aboutlib_version = "13.2.1"
|
||||||
leakcanary = "2.14"
|
leakcanary = "2.14"
|
||||||
moko = "0.25.2"
|
moko = "0.26.0"
|
||||||
okhttp_version = "5.3.2"
|
okhttp_version = "5.3.2"
|
||||||
shizuku_version = "13.1.5"
|
shizuku_version = "13.1.5"
|
||||||
sqldelight = "2.2.1"
|
sqldelight = "2.2.1"
|
||||||
sqlite = "2.6.2"
|
sqlite = "2.6.2"
|
||||||
voyager = "1.1.0-beta03"
|
voyager = "1.1.0-beta03"
|
||||||
spotless = "8.1.0"
|
spotless = "8.2.1"
|
||||||
ktlint-core = "1.8.0"
|
ktlint-core = "1.8.0"
|
||||||
firebase-bom = "34.7.0"
|
firebase-bom = "34.9.0"
|
||||||
markdown = "0.39.0"
|
markdown = "0.39.2"
|
||||||
junit = "6.0.1"
|
junit = "6.0.3"
|
||||||
|
materialKolor = "5.0.0-alpha06"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
|
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" }
|
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"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc"
|
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"
|
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-core = { module = "io.coil-kt.coil3:coil" }
|
||||||
coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
|
coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
|
||||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
|
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-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
||||||
kotest-assertions = "io.kotest:kotest-assertions-core:6.0.7"
|
kotest-assertions = "io.kotest:kotest-assertions-core:6.1.4"
|
||||||
mockk = "io.mockk:mockk:1.14.7"
|
mockk = "io.mockk:mockk:1.14.9"
|
||||||
|
|
||||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", 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" }
|
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version = "0.1.0" }
|
||||||
|
|
||||||
|
materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
||||||
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" }
|
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.3.12"
|
|||||||
|
|
||||||
versionsx = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
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"
|
exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -91,10 +91,10 @@
|
|||||||
<plurals name="num_lock_times">
|
<plurals name="num_lock_times">
|
||||||
<item quantity="zero">وقت الإنغلاق</item>
|
<item quantity="zero">وقت الإنغلاق</item>
|
||||||
<item quantity="one">وقت الإغلاق</item>
|
<item quantity="one">وقت الإغلاق</item>
|
||||||
<item quantity="two"/>
|
<item quantity="two">اوقات الإغلاق</item>
|
||||||
<item quantity="few"/>
|
<item quantity="few">اوقات الإغلاق</item>
|
||||||
<item quantity="many"/>
|
<item quantity="many">اوقات الإغلاق</item>
|
||||||
<item quantity="other"/>
|
<item quantity="other">اوقات الإغلاق</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="migrate_entry">
|
<plurals name="migrate_entry">
|
||||||
<item quantity="zero">ترحيل المدخل؟</item>
|
<item quantity="zero">ترحيل المدخل؟</item>
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
<string name="use_hentai_at_home">استخدم شبكة Hentai@Home</string>
|
<string name="use_hentai_at_home">استخدم شبكة Hentai@Home</string>
|
||||||
<string name="show_japanese_titles_option_2">يتم حاليًا عرض العناوين الإنجليزية/الحروف اللاتينية في نتائج البحث. امسح ذاكرة التخزين المؤقت للفصل بعد تغيير هذا (في القسم المتقدم)</string>
|
<string name="show_japanese_titles_option_2">يتم حاليًا عرض العناوين الإنجليزية/الحروف اللاتينية في نتائج البحث. امسح ذاكرة التخزين المؤقت للفصل بعد تغيير هذا (في القسم المتقدم)</string>
|
||||||
<string name="watched_tags_exh">فئاتExHentai التي شاهدتها</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_summary">سيتم تضمين المعارض التي تم تحميلها مؤخرًا على شاشة المشاهدة إذا كانت تحتوي على علامة مشاهدة واحدة على الأقل ذات وزن إيجابي، ويصل مجموع الأوزان على العلامات التي تمت مشاهدتها إلى هذه القيمة أو أعلى. يمكن تعيين هذه العتبة بين ٠ و٩٩٩٩. حاليًا القيمة :%1$d</string>
|
||||||
<string name="tag_watching_threshhold_error">يجب ان تكون القيمة بين صفر و ٩٩٩٩ !</string>
|
<string name="tag_watching_threshhold_error">يجب ان تكون القيمة بين صفر و ٩٩٩٩ !</string>
|
||||||
<string name="language_filtering_summary">إذا كنت ترغب في إخفاء المعارض التى بلغات معينة من قائمة المعرض والبحث، اختارها في الحوار الذى سوف يظهر.
|
<string name="language_filtering_summary">إذا كنت ترغب في إخفاء المعارض التى بلغات معينة من قائمة المعرض والبحث، اختارها في الحوار الذى سوف يظهر.
|
||||||
@@ -245,16 +245,7 @@
|
|||||||
<string name="show_updater_statistics">عرض إحصائيات التحديث</string>
|
<string name="show_updater_statistics">عرض إحصائيات التحديث</string>
|
||||||
<string name="gallery_updater_statistics_collection">جارٍ جمع الإحصائيات…</string>
|
<string name="gallery_updater_statistics_collection">جارٍ جمع الإحصائيات…</string>
|
||||||
<string name="gallery_updater_statistics">إحصائيات تحديث المعرض</string>
|
<string name="gallery_updater_statistics">إحصائيات تحديث المعرض</string>
|
||||||
<string name="gallery_updater_stats_time">"
|
<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>
|
||||||
\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="skip_page_restyling">تخطي إعادة تصميم الصفحة</string>
|
<string name="skip_page_restyling">تخطي إعادة تصميم الصفحة</string>
|
||||||
<string name="eh_settings_uploading_to_server">تحميل الإعدادات إلى الخادم</string>
|
<string name="eh_settings_uploading_to_server">تحميل الإعدادات إلى الخادم</string>
|
||||||
<string name="eh_settings_configuration_failed_message">حدث خطأ أثناء عملية التكوين:%1$s</string>
|
<string name="eh_settings_configuration_failed_message">حدث خطأ أثناء عملية التكوين:%1$s</string>
|
||||||
@@ -538,7 +529,7 @@
|
|||||||
<string name="favorites_sync_notes">ملاحظات مزامنة المفضلة الهامة</string>
|
<string name="favorites_sync_notes">ملاحظات مزامنة المفضلة الهامة</string>
|
||||||
<string name="eh_batch_add_finish">إنهاء</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="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="relation_alternate_story">قصة بديلة</string>
|
||||||
<string name="similar_titles">عناوين متشابهة</string>
|
<string name="similar_titles">عناوين متشابهة</string>
|
||||||
<string name="mangadex_preffered_source">مصدر MangaDex المفضل</string>
|
<string name="mangadex_preffered_source">مصدر MangaDex المفضل</string>
|
||||||
@@ -575,7 +566,7 @@
|
|||||||
<string name="page_preview_page_go_to">ادخل إلى</string>
|
<string name="page_preview_page_go_to">ادخل إلى</string>
|
||||||
<string name="rating2">مؤلم</string>
|
<string name="rating2">مؤلم</string>
|
||||||
<string name="no_rating">لا تقييم</string>
|
<string name="no_rating">لا تقييم</string>
|
||||||
<string name="artist_cg">الفنان</string>
|
<string name="artist_cg">فنان رسومات حاسوبية</string>
|
||||||
<string name="genre">النوع</string>
|
<string name="genre">النوع</string>
|
||||||
<string name="merged_toggle_download_chapters_error">خطأ في تنزيل الفصول</string>
|
<string name="merged_toggle_download_chapters_error">خطأ في تنزيل الفصول</string>
|
||||||
<string name="merged_references_invalid">اندماج المراجع غير صالح</string>
|
<string name="merged_references_invalid">اندماج المراجع غير صالح</string>
|
||||||
@@ -636,10 +627,15 @@
|
|||||||
<string name="select_scanlators">مجموعات المسح للعرض</string>
|
<string name="select_scanlators">مجموعات المسح للعرض</string>
|
||||||
<string name="relation_spin_off">تدور خارج</string>
|
<string name="relation_spin_off">تدور خارج</string>
|
||||||
<string name="doujinshi">دوجينشي</string>
|
<string name="doujinshi">دوجينشي</string>
|
||||||
<string name="non_h">Non-H</string>
|
<string name="non_h">غير إباحي</string>
|
||||||
<string name="asian_porn">إباحيات آسياوية</string>
|
<string name="asian_porn">إباحيات آسياوية</string>
|
||||||
<string name="id">المُعرّف</string>
|
<string name="id">المُعرّف</string>
|
||||||
<string name="is_exhentai_gallery">is Exhentai gallery</string>
|
<string name="is_exhentai_gallery">is Exhentai gallery</string>
|
||||||
<string name="language_translated">%1$s مترجم</string>
|
<string name="language_translated">%1$s مترجم</string>
|
||||||
<string name="relation_doujinshi">دوجينشي</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>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<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_migrate_now">Migrar ahora</string>
|
||||||
<string name="action_clean_titles">Limpiar títulos</string>
|
<string name="action_clean_titles">Limpiar títulos</string>
|
||||||
<string name="action_edit_info">Editar información</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="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="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_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="eh_image_quality_1280">1280x</string>
|
||||||
<string name="pref_enhanced_e_hentai_view">Exploración E/ExHentai mejorada</string>
|
<string name="pref_enhanced_e_hentai_view">Exploración E/ExHentai mejorada</string>
|
||||||
<string name="action_skip_entry">No migrar</string>
|
<string name="action_skip_entry">No migrar</string>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<string name="entry_type_webtoon">Webtoon</string>
|
<string name="entry_type_webtoon">Webtoon</string>
|
||||||
<string name="entry_type_manga">Manga</string>
|
<string name="entry_type_manga">Manga</string>
|
||||||
<string name="pref_category_all_sources">Todas las fuentes</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">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
|
<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:
|
\nOpciones:
|
||||||
@@ -39,27 +39,25 @@
|
|||||||
<string name="use_original_images">Utiliza imágenes originales</string>
|
<string name="use_original_images">Utiliza imágenes originales</string>
|
||||||
<string name="use_original_images_on">Actualmente usando 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="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="eh_image_quality_1600">1600x</string>
|
||||||
<string name="frong_page_categories">Categorías de la página principal</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_2400">2400x</string>
|
||||||
<string name="eh_image_quality_summary">La calidad de las imágenes descargadas</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">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="eh_image_quality_980">980x</string>
|
||||||
<string name="disable_favorites_uploading">Desactivar la subida de favoritos</string>
|
<string name="disable_favorites_uploading">Desactivar la subida de favoritos</string>
|
||||||
<string name="eh_image_quality_780">780x</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="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="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="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, selecciónalas en el cuadro de diálogo que aparecerá.
|
<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>
|
||||||
\nTen en cuenta que las galerías coincidentes nunca aparecerán, sin importar tu consulta de búsqueda.
|
|
||||||
\nTldr marcado = excluir</string>
|
|
||||||
<string name="watched_tags_exh">Etiquetas Observadas en ExHentai</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_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="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="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="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>
|
<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="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">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="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="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="force_sync_state_reset">Restablece el estado de sincronización</string>
|
||||||
<string name="sync_state_reset">Restablecimiento del 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="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="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="gallery_update_checker">Comprobar actualizaciones de la galería</string>
|
||||||
<string name="auto_update_restrictions">Restricciones para actualizaciones automáticas</string>
|
<string name="auto_update_restrictions">Restringir actualizaciones automáticas</string>
|
||||||
<string name="time_between_batches">Tiempo entre actualizaciones</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_never">Nunca actualizar las galerías</string>
|
||||||
<string name="time_between_batches_1_hour">1 hora</string>
|
<string name="time_between_batches_1_hour">1 hora</string>
|
||||||
<string name="time_between_batches_2_hours">2 horas</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_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="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="settings_profile_note">Nota de perfil de configuración</string>
|
||||||
<string name="eh_settings_successfully_uploaded">¡Ajustes cargados correctamente!</string>
|
<string name="eh_settings_successfully_uploaded">¡Los ajustes se han subido!</string>
|
||||||
<string name="eh_settings_configuration_failed">¡Error en la configuración!</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_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="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="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.
|
<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>
|
||||||
\n
|
<string name="eh_settings_uploading_to_server_message">Espera un momento, puede tardar un poco…</string>
|
||||||
\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_out_of_slots_error">¡No tienes espacio en tu perfil %1$s, primero elimina un perfil ya existente!</string>
|
||||||
<string name="eh_settings_uploading_to_server_message">Por favor espere, esto puede tardar algún tiempo…</string>
|
<string name="recheck_login_status">Recomprobar estado de inicio de sesión</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="alternative_login_page">Página de inicio de sesión alternativa</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="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="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="developer_tools">Herramientas de desarrollador</string>
|
||||||
<string name="toggle_hentai_features">Activar funciones integradas de hentai</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">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="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">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">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">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="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 la limpieza</string>
|
<string name="starting_cleanup">Comenzando el borrado</string>
|
||||||
<string name="clean_up_downloaded_chapters">Limpiar capítulos descargados</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="delete_unused_chapters">Elimina carpetas de capítulos inexistentes, parcialmente descargados, o ya leídos</string>
|
||||||
<string name="no_folders_to_cleanup">No hay carpetas para limpiar</string>
|
<string name="no_folders_to_cleanup">No hay ninguna carpeta que limpiar</string>
|
||||||
<string name="clean_orphaned_downloads">Limpiar huérfanos</string>
|
<string name="clean_orphaned_downloads">Borrar descargas huérfanas</string>
|
||||||
<string name="clean_read_downloads">Limpiar leídos</string>
|
<string name="clean_read_downloads">Borrar ya leídos</string>
|
||||||
<string name="clean_read_entries_not_in_library">Limpiar entradas no en la biblioteca</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">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_summary">Comprimir imágenes antes de descargarlas o verlas en el visor</string>
|
||||||
<string name="data_saver_downloader">Usar ahorrador de datos en el descargador</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_jpeg">Ignorar imágenes JPEG</string>
|
||||||
<string name="data_saver_ignore_gif">Ignorar animaciones GIF</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_quality">Calidad de imagen</string>
|
||||||
<string name="data_saver_image_format">Comprimir a Jpeg</string>
|
<string name="data_saver_image_format">Comprimir como 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.
|
<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>
|
||||||
\nActualmente, se comprime a 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_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 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_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_color_bw">Convertir en Blanco & Negro</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="bandwidth_hero">Bandwidth Hero ( Requiere un servidor proxy )</string>
|
||||||
<string name="wsrv">wsrv.nl</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="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="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_extra">Extra</string>
|
||||||
<string name="log_extreme">Extremo</string>
|
<string name="log_extreme">Extremo</string>
|
||||||
<string name="log_minimal_desc">Solo errores criticos</string>
|
<string name="log_minimal_desc">Solo errores críticos</string>
|
||||||
<string name="log_extra_desc">Todos los registros</string>
|
<string name="log_extra_desc">Registrar todos los mensajes</string>
|
||||||
<string name="log_extreme_desc">Modo de inspeccion de red</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="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_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_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_show_bottom_bar_labels">Mostrar siempre las etiquetas de navegación</string>
|
||||||
<string name="pref_category_navbar">Barra 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">Recomendaciones en menú lateral</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_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ú adicional</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="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_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>
|
<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">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="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="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_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="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>
|
<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="update_30min">Cada 30 minutos</string>
|
||||||
<string name="pref_source_source_filtering">Filtrar las fuentes en categorías</string>
|
<string name="pref_source_source_filtering">Filtrar las fuentes en categorías</string>
|
||||||
<string name="update_3hour">Cada 3 horas</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">Posición de la pestaña de novedades</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_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_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">Carpetas ocultas de fuente local</string>
|
||||||
<string name="pref_local_source_hidden_folders_summery">Permite a la fuente local leer carpetas ocultas</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="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_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_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_start_time">Introduce una hora de inicio</string>
|
||||||
<string name="biometric_lock_end_time">Introducir hora de finalización</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">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">Días de bloqueo biométrico</string>
|
||||||
<string name="biometric_lock_days_summary">Días para tener la aplicación bloqueada</string>
|
<string name="biometric_lock_days_summary">Días para tener la aplicación bloqueada</string>
|
||||||
<string name="sunday">Domingo</string>
|
<string name="sunday">Domingo</string>
|
||||||
@@ -242,7 +236,7 @@
|
|||||||
<string name="thursday">Jueves</string>
|
<string name="thursday">Jueves</string>
|
||||||
<string name="encrypt_database">Cifrar base de datos</string>
|
<string name="encrypt_database">Cifrar base de datos</string>
|
||||||
<string name="friday">Viernes</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="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">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>
|
<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_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="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="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="sync_favorites">Sincronizar favoritos de EH</string>
|
||||||
<string name="favorites_sync_reset">¿Estás seguro?</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>
|
<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_spin_off">Derivado de</string>
|
||||||
<string name="relation_alternate_story">Historia alternativa</string>
|
<string name="relation_alternate_story">Historia alternativa</string>
|
||||||
<string name="relation_alternate_version">Versión 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="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="could_not_open_entry">No se pudo abrir esta entrada:\n\n%1$s</string>
|
||||||
<string name="launching_app">Iniciando aplicación…</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="pref_crop_borders_continuous_vertical">Recortar el borde vertical</string>
|
||||||
<string name="humanize_fallback">hace unos instantes</string>
|
<string name="humanize_fallback">hace unos instantes</string>
|
||||||
<string name="pref_crop_borders_webtoon">Recortar bordes Webtoon</string>
|
<string name="pref_crop_borders_webtoon">Recortar bordes Webtoon</string>
|
||||||
<string name="feed">Feed</string>
|
<string name="feed">Novedades</string>
|
||||||
<string name="feed_delete">¿Borrar artículo de feed?</string>
|
<string name="feed_delete">¿Borrar el elemento de novedades?</string>
|
||||||
<string name="too_many_in_feed">Demasiadas fuentes en tu feed, no se pueden agregar más de 10</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="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="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="skip_pre_migration">Saltar pre-migración</string>
|
||||||
<string name="search_parameter">Buscar parámetro (p. ej. idioma:inglés)</string>
|
<string name="search_parameter">Buscar parámetro (p. ej. idioma:inglés)</string>
|
||||||
<string name="lewd">Lascivo</string>
|
<string name="lewd">Lascivo</string>
|
||||||
@@ -584,7 +578,7 @@
|
|||||||
<string name="page_count">Número de páginas</string>
|
<string name="page_count">Número de páginas</string>
|
||||||
<string name="parent">Padre</string>
|
<string name="parent">Padre</string>
|
||||||
<string name="uploader">Cargador</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="uploader_capital">Cargador principal</string>
|
||||||
<string name="follow_status">Seguir estado</string>
|
<string name="follow_status">Seguir estado</string>
|
||||||
<string name="language_translated">%1$s TR</string>
|
<string name="language_translated">%1$s TR</string>
|
||||||
@@ -610,7 +604,7 @@
|
|||||||
<string name="relation_monochrome">Monocromo</string>
|
<string name="relation_monochrome">Monocromo</string>
|
||||||
<string name="entry_not_tracked">El título no tiene seguimiento.</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="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="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_error_title">La búsqueda no se ha podido completar</string>
|
||||||
<string name="rec_common_recommendations">Recomendaciones en común</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_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_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="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_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 títulos a través de los metadatos de la fuente</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="scan_qr_code">Escanear un código QR</string>
|
||||||
<string name="final_chapter">Capítulo final</string>
|
<string name="final_chapter">Capítulo final</string>
|
||||||
<string name="file_extension">Extensión de archivo</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>
|
</resources>
|
||||||
|
|||||||
@@ -651,4 +651,6 @@
|
|||||||
<string name="file_extension">Extension ng file</string>
|
<string name="file_extension">Extension ng file</string>
|
||||||
<string name="final_chapter">Huling Kabanata</string>
|
<string name="final_chapter">Huling Kabanata</string>
|
||||||
<string name="base_url">Base url</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>
|
</resources>
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
<string name="pref_sorting_settings">Paramètres de tri</string>
|
<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="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">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_global">Toujours lancer des mises à jour globales</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_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>
|
<string name="library_group_updates_all">Lancer des mises à jour de catégorie tout le temps</string>
|
||||||
<!-- Browse settings -->
|
<!-- Browse settings -->
|
||||||
<string name="pref_source_navigation">Remplacer le dernier bouton</string>
|
<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="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="similar_titles">Titres similaires</string>
|
||||||
<string name="select_scanlators">Groupes de scantrad à afficher</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>
|
</resources>
|
||||||
|
|||||||
@@ -586,7 +586,7 @@
|
|||||||
<string name="deduplication_entry_info">Informacije unosa:</string>
|
<string name="deduplication_entry_info">Informacije unosa:</string>
|
||||||
<string name="follow_status">Stanje praćenja</string>
|
<string name="follow_status">Stanje praćenja</string>
|
||||||
<string name="language_translated">%1$s prevedeno</string>
|
<string name="language_translated">%1$s prevedeno</string>
|
||||||
<string name="media_id">ID madija</string>
|
<string name="media_id">ID medija</string>
|
||||||
<string name="last_update_check">Zadnja provjera aktualiziranja</string>
|
<string name="last_update_check">Zadnja provjera aktualiziranja</string>
|
||||||
<string name="watched_tags">Praćene oznake</string>
|
<string name="watched_tags">Praćene oznake</string>
|
||||||
<string name="watched_tags_summary">Otvara web prikaz za tvoju E/ExHentai stranicu praćenih oznaka</string>
|
<string name="watched_tags_summary">Otvara web prikaz za tvoju E/ExHentai stranicu praćenih oznaka</string>
|
||||||
@@ -620,4 +620,10 @@
|
|||||||
<string name="feed_tab_empty">Nemaš nijedan izvor u svom feedu, idi gore desno za dodavanje izvora</string>
|
<string name="feed_tab_empty">Nemaš nijedan izvor u svom feedu, idi gore desno za dodavanje izvora</string>
|
||||||
<string name="feed_add">Dodati %1$s u feed?</string>
|
<string name="feed_add">Dodati %1$s u feed?</string>
|
||||||
<string name="download_threads">Preuzmi teme</string>
|
<string name="download_threads">Preuzmi teme</string>
|
||||||
|
<string name="filename">Ime datoteke</string>
|
||||||
|
<string name="file_extension">Sufiks datoteke</string>
|
||||||
|
<string name="base_url">Osnovni URL</string>
|
||||||
|
<string name="final_chapter">Završno poglavlje</string>
|
||||||
|
<string name="pref_include_chapter_url_hash">Uključi hash URL-a poglavlja</string>
|
||||||
|
<string name="pref_include_chapter_url_hash_desc">Dodaj prvih šest znakova MD5 hash-a URL-a poglavlja imenu datoteke ili mapi poglavlja.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -336,4 +336,8 @@
|
|||||||
<string name="google_drive_login_failed">Impossibile accedere a Google Drive: %s</string>
|
<string name="google_drive_login_failed">Impossibile accedere a Google Drive: %s</string>
|
||||||
<string name="google_drive_not_signed_in">Non hai effettuato l\'accesso a Google Drive</string>
|
<string name="google_drive_not_signed_in">Non hai effettuato l\'accesso a Google Drive</string>
|
||||||
<string name="error_deleting_google_drive_lock_file">Errore durante l\'eliminazione del file di blocco di Google Drive</string>
|
<string name="error_deleting_google_drive_lock_file">Errore durante l\'eliminazione del file di blocco di Google Drive</string>
|
||||||
|
<string name="biometric_lock_start_time">Inserisci l\'orario di inizio</string>
|
||||||
|
<string name="delete_time_range">Elimina intervallo di tempo</string>
|
||||||
|
<string name="encrypt_database_subtitle">È necessario riavviare l\'applicazione affinché abbia effetto</string>
|
||||||
|
<string name="password_protect_downloads">Proteggi i download con password</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<plurals name="cleanup_done">
|
||||||
|
<item quantity="other">클린업 완료. %d 폴더를 삭제했습니다</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="num_lock_times">
|
||||||
|
<item quantity="other">%d 락 타임</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="eh_retry_toast">
|
||||||
|
<item quantity="other">실패한 페이지 %1$d 재시도 중…</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="pref_tag_sorting_desc">
|
||||||
|
<item quantity="other">정렬 리스트에 %1$d개 태그 추가. 라이브러리에 우선순위 기반 태그 목록을 기준으로 정렬하는 옵션을 추가합니다. 즉, 항목이 원하는 태그를 가진 항목을 우선하는 방식으로 정렬됩니다</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="migrate_entry">
|
||||||
|
<item quantity="other">%1$d%2$s 항목을 이동하시겠습니까?</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="copy_entry">
|
||||||
|
<item quantity="other">%1$d%2$s 항목을 복사하시겠습니까?</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="entry_migrated">
|
||||||
|
<item quantity="other">%d개 항목 이동됨</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="num_pages">
|
||||||
|
<item quantity="other">%1$d 페이지</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="browse_language_and_pages">
|
||||||
|
<item quantity="other">%2$s, %1$d 페이지</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_year">
|
||||||
|
<item quantity="other">%1$d년 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_month">
|
||||||
|
<item quantity="other">%1$d개월 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_week">
|
||||||
|
<item quantity="other">%1$d주 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_day">
|
||||||
|
<item quantity="other">%1$d일 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_hour">
|
||||||
|
<item quantity="other">%1$d시간 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_minute">
|
||||||
|
<item quantity="other">%1$d분 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="humanize_second">
|
||||||
|
<item quantity="other">%1$d초 전</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="row_count">
|
||||||
|
<item quantity="other">%d 행</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="action_skip_entry">이동하지 않음</string>
|
||||||
|
<string name="action_search_manually">수동 검색</string>
|
||||||
|
<string name="action_migrate_now">지금 이동</string>
|
||||||
|
<string name="action_copy_now">지금 복사</string>
|
||||||
|
<string name="action_clean_titles">타이틀 삭제</string>
|
||||||
|
<string name="action_start_reading">읽기 시작</string>
|
||||||
|
<string name="action_edit_info">정보 편집</string>
|
||||||
|
<string name="entry_type_manga">만화(일본)</string>
|
||||||
|
<string name="entry_type_manhwa">만화(한국)</string>
|
||||||
|
<string name="entry_type_manhua">만화(중국)</string>
|
||||||
|
<string name="entry_type_comic">코믹</string>
|
||||||
|
<string name="entry_type_webtoon">웹툰</string>
|
||||||
|
<string name="pref_category_all_sources">모든 소스</string>
|
||||||
|
<string name="pref_category_eh">E-Hentai</string>
|
||||||
|
<string name="pref_category_fork">포크 설정</string>
|
||||||
|
<string name="pref_category_mangadex">MangaDex</string>
|
||||||
|
<string name="pref_ehentai_summary">E/ExHentai 로그인, 갤러리 동기화</string>
|
||||||
|
<string name="pref_mangadex_summary">MangaDex 로그인, 팔로우 동기화</string>
|
||||||
|
<string name="changelog_version">버전 %1$s</string>
|
||||||
|
<string name="ehentai_prefs_account_settings">E-Hentai 웹사이트 계정 설정</string>
|
||||||
|
<string name="enable_exhentai">ExHentai 활성화</string>
|
||||||
|
<string name="requires_login">로그인 필요</string>
|
||||||
|
<string name="use_hentai_at_home">Hentai@Home 네트워크 사용</string>
|
||||||
|
<string name="use_hentai_at_home_summary">가능한 경우 Hentai@Home 네트워크를 통해 이미지를 불러오시겠습니까? 비활성화할 경우 볼 수 있는 페이지 수가 감소합니다\n옵션: \n- 모든 클라이언트 (추천)\n- 기본 포트 클라이언트만 (느려질 수 있음. 비표준 발송 포트를 차단하는 방화벽/프록시 뒤에 있는 경우 활성화)</string>
|
||||||
|
<string name="use_hentai_at_home_option_1">모든 클라이언트 (추천)</string>
|
||||||
|
<string name="use_hentai_at_home_option_2">기본 포트 클라이언트만</string>
|
||||||
|
<string name="show_japanese_titles">검색 결과를 일본어 제목으로 표시</string>
|
||||||
|
<string name="show_japanese_titles_option_1">현재 검색 결과에 일본어 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정)</string>
|
||||||
|
<string name="show_japanese_titles_option_2">현재 검색 결과에 영어/로마자 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정)</string>
|
||||||
|
<string name="use_original_images">원본 이미지 사용</string>
|
||||||
|
<string name="use_original_images_on">현재 원본 이미지를 사용하고 있습니다</string>
|
||||||
|
<string name="use_original_images_off">현재 리샘플된 이미지를 사용하고 있습니다</string>
|
||||||
|
<string name="watched_tags">관심 태그</string>
|
||||||
|
<string name="watched_tags_summary">웹뷰를 열어 E/ExHentai 관심 태그 페이지로 이동합니다</string>
|
||||||
|
<string name="watched_tags_exh">ExHentai 관심 태그</string>
|
||||||
|
<string name="tag_filtering_threshold">태그 필터 임계값</string>
|
||||||
|
<string name="tag_filtering_threshhold_error">-9999 ~ 0 사이의 값이어야 합니다!</string>
|
||||||
|
<string name="clear_db_exclude_read">읽은 챕터가 있는 항목을 유지</string>
|
||||||
|
<string name="artist">아티스트</string>
|
||||||
|
<string name="author">저자</string>
|
||||||
|
<string name="tag_filtering_threshhold_summary">태그에 음수 가중치를 부여해 \'내 태크\'의 E/ExHentai 페이지에 추가하면 태그를 소프트 필터할 수 있습니다. 갤러리의 태그 가중치가 이 값보다 낮을 경우 표시되지 않습니다. 임계값은 -9999에서 0까지 설정할 수 있습니다. 현재: %1$d</string>
|
||||||
|
<string name="tag_watching_threshhold">태그 열람 임계값</string>
|
||||||
|
<string name="tag_watching_threshhold_error">0 ~ 9999 사이의 값이어야 합니다!</string>
|
||||||
|
<string name="tag_watching_threshhold_summary">최근 업로드된 갤러리는 양수 가중치의 최소 한 개의 관심 태그가 있으며, 갤러리의 관심 태그 가중치 합계가 이 값보다 높을 경우 관심 화면에 포함됩니다. 이 임계값은 0에서 9999 사이로 설정할 수 있습니다. 현재: %1$d</string>
|
||||||
|
<string name="language_filtering">언어 필터</string>
|
||||||
|
<string name="language_filtering_summary">갤러리 및 검색 목록에서 특정 언어를 숨기고 싶은 경우, 대화상자에서 해당 갤러리를 선택하십시오.\n해당 갤러리는 검색 결과와 무관하게 표시되지 않습니다.\n요약: 체크 표시 = 제외됨</string>
|
||||||
|
<string name="frong_page_categories">첫 페이지 카테고리</string>
|
||||||
|
<string name="fromt_page_categories_summary">첫 페이지와 검색 결과에서 기본적으로 어느 카테고리를 표시하시겠습니까? 해당 필터를 활성화하면 사용할 수 있습니다</string>
|
||||||
|
<string name="watched_list_default">관심 목록 필터 기본 상태</string>
|
||||||
|
<string name="watched_list_state_summary">ExHentai/E-Hentai를 탐색할 때 관심 목록 필터를 기본적으로 활성화합니다</string>
|
||||||
|
<string name="eh_image_quality_summary">다운로드 이미지 품질</string>
|
||||||
|
<string name="eh_image_quality">이미지 품질</string>
|
||||||
|
<string name="eh_image_quality_auto">자동</string>
|
||||||
|
<string name="eh_image_quality_2400">2400x</string>
|
||||||
|
<string name="eh_image_quality_1600">1600x</string>
|
||||||
|
<string name="eh_image_quality_1280">1280x</string>
|
||||||
|
<string name="eh_image_quality_980">980x</string>
|
||||||
|
<string name="eh_image_quality_780">780x</string>
|
||||||
|
<string name="pref_enhanced_e_hentai_view">고급 E/ExHentai 탐색</string>
|
||||||
|
<string name="pref_enhanced_e_hentai_view_summary">E/ExHentai 용도로 제작된 고급 탐색 메뉴 활성화/비활성화</string>
|
||||||
|
<string name="favorites_sync">E-Hentai 즐겨찾기 동기화</string>
|
||||||
|
<string name="disable_favorites_uploading">즐겨찾기 업로드 비활성화</string>
|
||||||
|
<string name="disable_favorites_uploading_summary">즐겨찾기는 ExHentai에서만 다운로드됩니다. 앱 내 즐겨찾기 변경사항은 업로드되지 않습니다. 이는 ExHentai에서 즐겨찾기가 실수로 제거되는 것을 방지합니다. 다만 제거된 항목은 여전히 다운로드됩니다(ExHentai에서 즐겨찾기를 제거하면 앱에서도 제거됩니다).</string>
|
||||||
|
<string name="show_favorite_sync_notes">즐겨찾기 동기화 노트 표시</string>
|
||||||
|
<string name="show_favorite_sync_notes_summary">즐겨찾기 동기화 기능에 대한 정보를 표시합니다</string>
|
||||||
|
<string name="please_login">로그인해 주십시오!</string>
|
||||||
|
<string name="ignore_sync_errors">가능한 경우 동기화 오류를 무시</string>
|
||||||
|
<string name="ignore_sync_errors_summary">동기화 중 오류가 발생해도 즉시 중단하지 않습니다. 오류 내용은 동기화가 완료되었을 때 표시됩니다. 경우에 따라 즐겨찾기 정보가 손상될 수 있습니다. 대용량 라이브러리 동기화에 유용합니다.</string>
|
||||||
|
<string name="force_sync_state_reset">동기화 상태 강제 리셋</string>
|
||||||
|
<string name="force_sync_state_reset_summary">다음 동기화 때 전체 재동기화를 수행합니다. 삭제는 동기화되지 않습니다. 앱의 모든 즐겨찾기를 Exhentai에 다시 업로드하며 Exhentai의 모든 즐겨찾기를 앱에 다시 다운로드합니다. 동기화가 중단된 후 복구할 때에 유용합니다.</string>
|
||||||
|
<string name="sync_state_reset">동기화 상태 초기화</string>
|
||||||
|
<string name="gallery_update_checker">갤러리 업데이트 확인</string>
|
||||||
|
<string name="auto_update_restrictions">자동 업데이트 제한</string>
|
||||||
|
<string name="time_between_batches">업데이트 배치 시간 간격</string>
|
||||||
|
<string name="time_between_batches_never">갤러리를 업데이트하지 않음</string>
|
||||||
|
<string name="time_between_batches_1_hour">1시간</string>
|
||||||
|
<string name="time_between_batches_2_hours">2시간</string>
|
||||||
|
<string name="time_between_batches_3_hours">3시간</string>
|
||||||
|
<string name="time_between_batches_6_hours">6시간</string>
|
||||||
|
<string name="time_between_batches_12_hours">12시간</string>
|
||||||
|
<string name="time_between_batches_24_hours">24시간</string>
|
||||||
|
<string name="time_between_batches_48_hours">48시간</string>
|
||||||
|
<string name="time_between_batches_summary_1">%1$s(은)는 현재 라이브러리에서 갤러리 업데이트를 확인하지 않습니다.</string>
|
||||||
|
<string name="show_updater_statistics">업데이트 통계 표시</string>
|
||||||
|
<string name="gallery_updater_statistics_collection">통계 집계 중…</string>
|
||||||
|
<string name="gallery_updater_statistics">갤러리 업데이트 통계</string>
|
||||||
|
<string name="gallery_updater_not_ran_yet">업데이트가 아직 실행되지 않음.</string>
|
||||||
|
<string name="gallery_updater_stats_time">\n마지막으로 확인된 갤러리:\n- 시간: %1$d\n- 6시간: %2$d\n- 12시간: %3$d\n- 일: %4$d\n- 2일: %5$d\n- 주: %6$d\n- 월: %7$d\n- 연: %8$d</string>
|
||||||
|
<string name="settings_profile_note">프로필 노트 설정</string>
|
||||||
|
<string name="settings_profile_note_message">앱 성능 최적화를 위해 E-Hentai 및 ExHentai에 새 설정 프로필이 추가됩니다. 두 사이트의 프로필이 3개 이하인지 확인해 주십시오.\n\n설정 프로필이 무엇인지 모르는 경우, 중요하지 않을 것이므로 「OK」를 눌러 주십시오.</string>
|
||||||
|
<string name="eh_settings_successfully_uploaded">설정이 업로드되었습니다!</string>
|
||||||
|
<string name="eh_settings_configuration_failed">설정에 실패했습니다!</string>
|
||||||
|
<string name="eh_settings_configuration_failed_message">설정 처리 중 오류 발생: %1$s</string>
|
||||||
|
<string name="eh_settings_uploading_to_server">설정을 서버로 업로드 중</string>
|
||||||
|
<string name="eh_settings_uploading_to_server_message">잠시 기다려 주십시오, 시간이 걸립니다…</string>
|
||||||
|
<string name="eh_settings_out_of_slots_error">%1$s 프로필 슬롯이 부족합니다. 프로필을 삭제해 주십시오!</string>
|
||||||
|
<string name="recheck_login_status">로그인 상태 재확인</string>
|
||||||
|
<string name="alternative_login_page">대체 로그인 페이지</string>
|
||||||
|
<string name="skip_page_restyling">페이지 스타일 변경 넘어가기</string>
|
||||||
|
<string name="custom_igneous_cookie">커스텀 igneous 쿠키</string>
|
||||||
|
<string name="custom_igneous_cookie_message">일부 사용자는 ExHentai에 정상적인 방법으로 액세스할 수 없으며, 특정 igneous 쿠키 값을 전달해야 합니다. 이 옵션은 해당 사용자를 위한 것입니다.</string>
|
||||||
|
<string name="developer_tools">개발자 도구</string>
|
||||||
|
<string name="toggle_hentai_features">통합 hentai 기능 활성화</string>
|
||||||
|
<string name="toggle_hentai_features_summary">비활성화하면 모든 hentai 기능을 비활성화하는 실험적 기능</string>
|
||||||
|
<string name="toggle_delegated_sources">위임된 소스 활성화</string>
|
||||||
|
<string name="toggle_delegated_sources_summary">다음 소스가 설치된 경우 %1$s 향상 적용: %2$s</string>
|
||||||
|
<string name="log_level">로그 레벨</string>
|
||||||
|
<string name="log_level_summary">앱 성능에 영향을 미치는 설정입니다. 변경 후 앱을 강제로 재시작합니다. 현재 값: %s</string>
|
||||||
|
<string name="enable_source_blacklist">소스 블랙리스트 활성화</string>
|
||||||
|
<string name="enable_source_blacklist_summary">%1$s 호환되지 않는 확장자/소스를 숨깁니다. 변경 후 앱을 강제로 재시작합니다.</string>
|
||||||
|
<string name="open_debug_menu">디버그 메뉴 열기</string>
|
||||||
|
<string name="open_debug_menu_summary"><![CDATA[무엇을 하는지 이해하지 못한다면 이 메뉴를 건드리지 마십시오! <font color=\'red\'>라이브러리가 망가질 수 있습니다!</font>]]></string>
|
||||||
|
<string name="starting_cleanup">정리 시작 중</string>
|
||||||
|
<string name="clean_up_downloaded_chapters">다운로드한 챕터 정리</string>
|
||||||
|
<string name="delete_unused_chapters">존재하지 않는 폴더, 부분적으로 다운로드된 폴더, 읽은 챕터 폴더를 삭제합니다</string>
|
||||||
|
<string name="no_folders_to_cleanup">정리할 폴더 없음</string>
|
||||||
|
<string name="clean_orphaned_downloads">고립된 폴더 삭제</string>
|
||||||
|
<string name="clean_read_downloads">읽은 폴더 삭제</string>
|
||||||
|
<string name="clean_read_entries_not_in_library">라이브러리에 없는 항목 삭제</string>
|
||||||
|
<string name="data_saver">데이터 세이버</string>
|
||||||
|
<string name="data_saver_summary">리더에서 다운로드 또는 불러오기 전에 이미지를 압축</string>
|
||||||
|
<string name="data_saver_downloader">다운로더에 데이터 세이버 사용</string>
|
||||||
|
<string name="data_saver_ignore_jpeg">Jpeg 이미지 제외</string>
|
||||||
|
<string name="data_saver_ignore_gif">Gif 애니메이션 제외</string>
|
||||||
|
<string name="data_saver_image_quality">이미지 화질</string>
|
||||||
|
<string name="data_saver_image_quality_summary">값이 높을수록 이미지 화질이 더 많이 보존되지만 파일 크기가 커집니다. 80 퍼센트는 파일 크기와 이미지 화질 사이의 좋은 타협점입니다</string>
|
||||||
|
<string name="data_saver_image_format">Jpeg로 압축</string>
|
||||||
|
<string name="data_saver_image_format_summary_on">Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Jpeg로 압축</string>
|
||||||
|
<string name="data_saver_image_format_summary_off">Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Webp로 압축</string>
|
||||||
|
<string name="data_saver_color_bw">흑백으로 변환</string>
|
||||||
|
<string name="bandwidth_hero">Bandwidth Hero (Bandwidth Hero 프록시 서버 필요)</string>
|
||||||
|
<string name="wsrv">wsrv.nl</string>
|
||||||
|
<string name="bandwidth_data_saver_server">Bandwidth Hero 프록시 서버</string>
|
||||||
|
<string name="data_saver_server_summary">여기에 Bandwidth Hero 프록시 서버 url 입력</string>
|
||||||
|
<string name="pref_include_chapter_url_hash">챕터 URL 해시 포함</string>
|
||||||
|
<string name="pref_include_chapter_url_hash_desc">챕터 URL의 MD5 해시의 처음 여섯 글자를 챕터 파일 또는 폴더 이름에 추가합니다.</string>
|
||||||
|
<string name="log_minimal">최소</string>
|
||||||
|
<string name="log_extra">확장</string>
|
||||||
|
<string name="log_extreme">최대</string>
|
||||||
|
<string name="log_minimal_desc">치명적 오류만</string>
|
||||||
|
<string name="log_extra_desc">전체 로그</string>
|
||||||
|
<string name="log_extreme_desc">네트워크 검사 모드</string>
|
||||||
|
<string name="toggle_expand_search_filters">모든 검색 필터를 기본으로 확장</string>
|
||||||
|
</resources>
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
<string name="data_saver_downloader">Usar economia de dados no baixador</string>
|
<string name="data_saver_downloader">Usar economia de dados no baixador</string>
|
||||||
<string name="bandwidth_hero">Bandwidth Hero (Requer um servidor proxy Bandwidth Hero)</string>
|
<string name="bandwidth_hero">Bandwidth Hero (Requer um servidor proxy Bandwidth Hero)</string>
|
||||||
<string name="wsrv">wsrv.nl</string>
|
<string name="wsrv">wsrv.nl</string>
|
||||||
<string name="put_merge_in_overflow">Mesclar em transborde</string>
|
<string name="put_merge_in_overflow">Mesclar em caso de excesso</string>
|
||||||
<string name="put_merge_in_overflow_summary">Colocar o botão de mesclar no menu flutuante ao invés da pagina inicial</string>
|
<string name="put_merge_in_overflow_summary">Colocar o botão de mesclar no menu flutuante ao invés da pagina inicial</string>
|
||||||
<string name="pref_feed_position_summery">Você quer que a aba Feed seja a primeira aba em Navegar? Isso irá fazer dela a aba principal ao abrir Navegar, não é recomendado se você estiver usando uma rede medida ou dados móveis</string>
|
<string name="pref_feed_position_summery">Você quer que a aba Feed seja a primeira aba em Navegar? Isso irá fazer dela a aba principal ao abrir Navegar, não é recomendado se você estiver usando uma rede medida ou dados móveis</string>
|
||||||
<string name="label_triggers">Gatilhos</string>
|
<string name="label_triggers">Gatilhos</string>
|
||||||
@@ -541,7 +541,7 @@
|
|||||||
<string name="pref_sync_interval">Frequência de sincronização</string>
|
<string name="pref_sync_interval">Frequência de sincronização</string>
|
||||||
<string name="error_deleting_google_drive_lock_file">Error ao Deletar Ficheiro de Bloqueio no Google Drive</string>
|
<string name="error_deleting_google_drive_lock_file">Error ao Deletar Ficheiro de Bloqueio no Google Drive</string>
|
||||||
<string name="pref_sync_options_summ">Pode ser usado para criar gatilhos de sincronização</string>
|
<string name="pref_sync_options_summ">Pode ser usado para criar gatilhos de sincronização</string>
|
||||||
<string name="sync_on_chapter_read">Sincronizar após Leitura de Capitulo</string>
|
<string name="sync_on_chapter_read">Sincronizar com a leitura do capítulo</string>
|
||||||
<string name="pref_purge_confirmation_title">Confirmação de Limpeza</string>
|
<string name="pref_purge_confirmation_title">Confirmação de Limpeza</string>
|
||||||
<string name="pref_sync_options">Criar gatilhos de sincronização</string>
|
<string name="pref_sync_options">Criar gatilhos de sincronização</string>
|
||||||
<string name="sync_on_chapter_open">Sincronizar após abrir capítulo</string>
|
<string name="sync_on_chapter_open">Sincronizar após abrir capítulo</string>
|
||||||
@@ -655,4 +655,11 @@
|
|||||||
<string name="relation_prequel">Prólogo</string>
|
<string name="relation_prequel">Prólogo</string>
|
||||||
<string name="dedupe_priority">Eliminar duplicatas com base na prioridade</string>
|
<string name="dedupe_priority">Eliminar duplicatas com base na prioridade</string>
|
||||||
<string name="no_dedupe">Sem remoção de duplicatas</string>
|
<string name="no_dedupe">Sem remoção de duplicatas</string>
|
||||||
|
<string name="pref_include_chapter_url_hash">Incluir o hash da URL do capítulo</string>
|
||||||
|
<string name="pref_include_chapter_url_hash_desc">Adicione os seis primeiros caracteres do hash MD5 da URL do capítulo ao nome do arquivo ou pasta do capítulo.</string>
|
||||||
|
<string name="pref_tracker_resolve_using_source_metadata">Selecione entradas usando metadados de origem</string>
|
||||||
|
<string name="pref_tracker_resolve_using_source_metadata_summary">Seleciona automaticamente o título correspondente se a fonte fornecer links para rastreadores. Atualmente compatível com MangaDex</string>
|
||||||
|
<string name="encrypt_database_message"><![CDATA[<font color=\'red\'>HABILITAR ESTA OPÇÃO CRIARÁ UM NOVO BANCO DE DADOS. SIGA ESTAS EPATAS PARA MANTER SEUS DADOS SEGUROS<br>1. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> CRIAR BACKUP<br>2. CONFIGURAÇÕES DO SISTEMA -> LIMPAR DADOS DO APP<br>3. ABRA O APP E ATIVE ISSO<br>4. CONFIGURAÇÕES DO SISTEMA -> FORÇAR REINICIALIZAÇÃO<br>5. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> RESTAURAR BACKUP</font>]]></string>
|
||||||
|
<string name="aes_256">AES 256</string>
|
||||||
|
<string name="aes_128">AES 128</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user