Compare commits
109 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 | |||
| f8f645772d | |||
| b1e6fa65d6 | |||
| 01e8c6cc12 | |||
| b4668c6829 | |||
| 08d6c604bc | |||
| 02cec06535 | |||
| ebdb3f7478 | |||
| 3724d79825 | |||
| c3e2eb6672 | |||
| fa91695add | |||
| e7786bd16f | |||
| 3d70476b9f | |||
| e74e0de8f5 | |||
| a2f552d6d2 | |||
| a6bd0bbd2a | |||
| fd42bba188 | |||
| a0ec735066 | |||
| 89f5fce19d | |||
| bf711a995c | |||
| d977614b7a | |||
| d282df6973 | |||
| db5b3a69cc | |||
| c70c5dff25 | |||
| 25ace80419 | |||
| b8b468cea7 | |||
| 0ffc798e9a | |||
| ad5a76741a | |||
| 003c5ad39a | |||
| 582d0ef121 | |||
| 5566db160b | |||
| 6fb6838656 | |||
| 1e5d490c22 | |||
| 276aeb0f59 | |||
| c62d9d1446 | |||
| 4ff18364d9 | |||
| 6c8e4e951a | |||
| dc1fde628d | |||
| 241b70e5ce |
@@ -100,5 +100,5 @@ body:
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
- label: I understand that **TachiyomiSY does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
required: true
|
||||
|
||||
@@ -12,22 +12,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew spotlessCheck assembleDevDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: TachiyomiSY-${{ github.sha }}.apk
|
||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||
|
||||
@@ -15,20 +15,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
# SY -->
|
||||
- name: Write google-services.json
|
||||
|
||||
@@ -12,16 +12,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Create Tag
|
||||
run: |
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check PR and Add Label
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
@@ -60,7 +60,6 @@ Additional features for some extensions, features include custom description, op
|
||||
* Mangadex
|
||||
* NHentai
|
||||
* Puruin
|
||||
* Tsumino
|
||||
* LANraragi
|
||||
|
||||
## Download
|
||||
|
||||
@@ -150,12 +150,14 @@ kotlin {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xannotation-default-target=param-property",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -265,6 +267,7 @@ dependencies {
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.bundles.markdown)
|
||||
implementation(libs.materialKolor)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
@@ -60,6 +60,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -156,6 +157,7 @@ class DomainModule : InjektModule {
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
|
||||
|
||||
+13
-12
@@ -30,8 +30,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.gowtham.ratingbar.RatingBar
|
||||
import com.gowtham.ratingbar.RatingBarConfig
|
||||
import com.gowtham.ratingbar.ComposeStars
|
||||
import com.gowtham.ratingbar.RatingBarStyle
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import exh.metadata.MetadataUtil
|
||||
@@ -222,17 +222,18 @@ fun BrowseSourceEHentaiListItem(
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
RatingBar(
|
||||
ComposeStars(
|
||||
value = rating,
|
||||
onValueChange = {},
|
||||
onRatingChanged = {},
|
||||
config = RatingBarConfig().apply {
|
||||
isIndicator(true)
|
||||
numStars(5)
|
||||
size(18.dp)
|
||||
activeColor(Color(0xFF005ED7))
|
||||
inactiveColor(Color(0xE1E2ECFF))
|
||||
},
|
||||
numOfStars = 5,
|
||||
size = 18.dp,
|
||||
spaceBetween = 2.dp,
|
||||
hideInactiveStars = false,
|
||||
style = RatingBarStyle.Fill(
|
||||
activeColor = Color(0xFF005ED7),
|
||||
inActiveColor = Color(0xE1E2ECFF),
|
||||
),
|
||||
painterEmpty = null,
|
||||
painterFilled = null,
|
||||
)
|
||||
val color = genre?.first?.color
|
||||
val res = genre?.second
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ package eu.kanade.presentation.browse.components
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
@@ -17,7 +17,7 @@ fun BrowseSourceFloatingActionButton(
|
||||
onFabClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||
|
||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||
onClick = onCreate,
|
||||
|
||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(it.title)
|
||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
||||
// Don't show search action
|
||||
} else if (searchQuery == null) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_search))
|
||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
||||
}
|
||||
} else if (searchQuery.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_reset))
|
||||
|
||||
@@ -22,7 +22,7 @@ fun relativeDateText(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.takeIf { dateEpochMillis > 0L },
|
||||
.takeIf { dateEpochMillis != 0L },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun DownloadDropdownMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
offset: DpOffset? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (offset != null) {
|
||||
DropdownMenu(
|
||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
private fun DownloadDropdownMenuItems(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
) {
|
||||
@@ -59,6 +58,7 @@ private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||
)
|
||||
|
||||
options.map { (downloadAction, string) ->
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -27,9 +24,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -101,7 +100,6 @@ import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -167,7 +165,7 @@ fun MangaScreen(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -331,7 +329,7 @@ private fun MangaScreenSmallImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -418,25 +416,23 @@ private fun MangaScreenSmallImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
@@ -529,7 +525,7 @@ private fun MangaScreenSmallImpl(
|
||||
// SY -->
|
||||
doSearch = onSearch,
|
||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||
SearchMetadataChips(state.meta, state.source, state.manga.genre)
|
||||
SearchMetadataChips(state.meta, state.source.id, state.manga.genre)
|
||||
},
|
||||
// SY <--
|
||||
)
|
||||
@@ -654,7 +650,7 @@ fun MangaScreenLargeImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -737,27 +733,25 @@ fun MangaScreenLargeImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
PullRefresh(
|
||||
@@ -824,7 +818,7 @@ fun MangaScreenLargeImpl(
|
||||
// SY -->
|
||||
doSearch = onSearch,
|
||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||
SearchMetadataChips(state.meta, state.source, state.manga.genre)
|
||||
SearchMetadataChips(state.meta, state.source.id, state.manga.genre)
|
||||
},
|
||||
// SY <--
|
||||
)
|
||||
@@ -953,7 +947,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||
// SY <--
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
) {
|
||||
items(
|
||||
@@ -1020,14 +1014,14 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
onChapterSelected(item, !item.selected, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
||||
NEXT_10_CHAPTERS,
|
||||
NEXT_25_CHAPTERS,
|
||||
UNREAD_CHAPTERS,
|
||||
BOOKMARKED_CHAPTERS,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
|
||||
@@ -93,10 +93,10 @@ fun MangaBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
@@ -260,10 +260,10 @@ fun LibraryBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
|
||||
@@ -605,44 +605,47 @@ private fun ColumnScope.MangaContentInfo(
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
@Composable
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||
markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -25,8 +24,6 @@ import eu.kanade.presentation.components.ChipBorder
|
||||
import eu.kanade.presentation.components.SuggestionChip
|
||||
import eu.kanade.presentation.components.SuggestionChipDefaults
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
@@ -49,7 +46,7 @@ value class SearchMetadataChips(
|
||||
val tags: Map<String, List<DisplayTag>>,
|
||||
) {
|
||||
companion object {
|
||||
operator fun invoke(meta: RaisedSearchMetadata?, source: Source, tags: List<String>?): SearchMetadataChips? {
|
||||
operator fun invoke(meta: RaisedSearchMetadata?, sourceId: Long, tags: List<String>?): SearchMetadataChips? {
|
||||
return if (meta != null) {
|
||||
SearchMetadataChips(
|
||||
meta.tags
|
||||
@@ -59,11 +56,11 @@ value class SearchMetadataChips(
|
||||
namespace = it.namespace,
|
||||
text = it.name,
|
||||
search = if (!it.namespace.isNullOrEmpty()) {
|
||||
SourceTagsUtil.getWrappedTag(source.id, namespace = it.namespace, tag = it.name)
|
||||
SourceTagsUtil.getWrappedTag(sourceId, namespace = it.namespace, tag = it.name)
|
||||
} else {
|
||||
SourceTagsUtil.getWrappedTag(source.id, fullTag = it.name)
|
||||
SourceTagsUtil.getWrappedTag(sourceId, fullTag = it.name)
|
||||
} ?: it.name,
|
||||
border = if (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID) {
|
||||
border = if (sourceId == EXH_SOURCE_ID || sourceId == EH_SOURCE_ID) {
|
||||
when (it.type) {
|
||||
EHentaiSearchMetadata.TAG_TYPE_NORMAL -> 2
|
||||
EHentaiSearchMetadata.TAG_TYPE_LIGHT -> 1
|
||||
@@ -178,7 +175,6 @@ fun TagsChip(
|
||||
fun NamespaceTagsPreview() {
|
||||
TachiyomiPreviewTheme {
|
||||
Surface {
|
||||
val context = LocalContext.current
|
||||
NamespaceTags(
|
||||
tags = remember {
|
||||
EHentaiSearchMetadata().apply {
|
||||
@@ -216,7 +212,7 @@ fun NamespaceTagsPreview() {
|
||||
),
|
||||
),
|
||||
)
|
||||
}.let { SearchMetadataChips(it, EHentai(EXH_SOURCE_ID, true, context), emptyList()) }!!
|
||||
}.let { SearchMetadataChips(it, EXH_SOURCE_ID, emptyList()) }!!
|
||||
},
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
+21
@@ -89,6 +89,7 @@ import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetAllManga
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
@@ -117,6 +118,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
@@ -167,6 +169,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(libraryPreferences = libraryPreferences),
|
||||
getDownloadsGroup(downloadPreferences = downloadPreferences),
|
||||
getReaderGroup(basePreferences = basePreferences),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
// SY -->
|
||||
@@ -378,6 +381,24 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
)
|
||||
}
|
||||
|
||||
// SY ->
|
||||
@Composable
|
||||
private fun getDownloadsGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_downloads),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.includeChapterUrlHash(),
|
||||
title = stringResource(SYMR.strings.pref_include_chapter_url_hash),
|
||||
subtitle = stringResource(SYMR.strings.pref_include_chapter_url_hash_desc),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// <- SY
|
||||
|
||||
@Composable
|
||||
private fun getReaderGroup(
|
||||
basePreferences: BasePreferences,
|
||||
|
||||
@@ -245,7 +245,6 @@ object AboutScreen : Screen() {
|
||||
is GetApplicationRelease.Result.OsTooOld -> {
|
||||
context.toast(MR.strings.update_check_eol)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
+5
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||
LibrariesContainer(
|
||||
libraries = libraries,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ class DebugInfoScreen : Screen() {
|
||||
val status by produceState(initialValue = "-") {
|
||||
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
|
||||
value = when (result) {
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE_INSTALLED -> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
|
||||
"Compiled non-matching"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.presentation.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
||||
isAmoled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||
getThemeColorScheme(
|
||||
context = context,
|
||||
appTheme = appTheme,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getThemeColorScheme(
|
||||
context: Context,
|
||||
appTheme: AppTheme,
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
MonetColorScheme(context)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
||||
private val surfaceContainerHigh = Color(0xFF131313)
|
||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
fun getColorScheme(
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
overrideDarkSurfaceContainers: Boolean,
|
||||
): ColorScheme {
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
val amoledScheme = darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||
|
||||
return amoledScheme.copy(
|
||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||
surfaceContainerLowest = surfaceContainer,
|
||||
surfaceContainerLow = surfaceContainer,
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.UiModeManager
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
||||
import com.google.android.material.color.utilities.SchemeContent
|
||||
import com.google.android.material.color.utilities.Score
|
||||
import com.materialkolor.PaletteStyle
|
||||
import com.materialkolor.dynamiccolor.ColorSpec
|
||||
import com.materialkolor.ktx.DynamicScheme
|
||||
import com.materialkolor.toColorScheme
|
||||
|
||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
?.primaryColor
|
||||
?.toArgb()
|
||||
if (seed != null) {
|
||||
MonetCompatColorScheme(context, seed)
|
||||
MonetCompatColorScheme(Color(seed))
|
||||
} else {
|
||||
TachiyomiColorScheme
|
||||
}
|
||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme
|
||||
get() = monet.lightScheme
|
||||
|
||||
companion object {
|
||||
@Suppress("Unused")
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val bitmapPixels = IntArray(width * height)
|
||||
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
|
||||
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
|
||||
.takeIf { it != 0 } // Don't take fallback color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
||||
override val darkScheme = dynamicDarkColorScheme(context)
|
||||
}
|
||||
|
||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
||||
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||
|
||||
companion object {
|
||||
private fun Int.toComposeColor(): Color = Color(this)
|
||||
|
||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
||||
val scheme = SchemeContent(
|
||||
Hct.fromInt(seed),
|
||||
dark,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
)
|
||||
val dynamicColors = MaterialDynamicColors()
|
||||
return ColorScheme(
|
||||
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
|
||||
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
|
||||
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
|
||||
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
|
||||
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
|
||||
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
|
||||
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
|
||||
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
|
||||
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
|
||||
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
|
||||
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
|
||||
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
|
||||
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
|
||||
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
|
||||
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
|
||||
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
|
||||
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
|
||||
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
|
||||
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
|
||||
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
|
||||
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
|
||||
scrim = Color.Black,
|
||||
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
|
||||
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
|
||||
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||
return DynamicScheme(
|
||||
seedColor = seed,
|
||||
isDark = dark,
|
||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||
style = PaletteStyle.Expressive,
|
||||
)
|
||||
.toColorScheme(isAmoled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.platform.toClipEntry
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
@@ -73,6 +75,7 @@ import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
@@ -248,6 +251,7 @@ private fun SearchResultItem(
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
||||
expanded = dropDownMenuExpanded,
|
||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||
onCopyName = {
|
||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
||||
scope.launch {
|
||||
val clipEntry = ClipData.newPlainText(
|
||||
trackSearch.title,
|
||||
trackSearch.title,
|
||||
).toClipEntry()
|
||||
clipboard.setClipEntry(clipEntry)
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = {
|
||||
val url = trackSearch.tracking_url
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -30,18 +26,13 @@ fun TrackLogoIcon(
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
)
|
||||
}
|
||||
.clip(MaterialTheme.shapes.medium),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
|
||||
-4
@@ -1,8 +1,6 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.test.DummyTracker
|
||||
|
||||
@@ -13,8 +11,6 @@ internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker>
|
||||
DummyTracker(
|
||||
id = 1L,
|
||||
name = "Dummy Tracker",
|
||||
valLogoColor = Color.rgb(18, 25, 35),
|
||||
valLogo = R.drawable.ic_tracker_anilist,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun UpdatesFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(screenModel = screenModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterSheet(
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = filterDownloaded,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
)
|
||||
|
||||
val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
)
|
||||
|
||||
val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
)
|
||||
|
||||
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
|
||||
|
||||
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState()
|
||||
|
||||
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { toggleScanlatorFilter() }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_filter_excluded_scanlators),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = filterExcludedScanlators,
|
||||
onCheckedChange = { toggleScanlatorFilter() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -37,6 +40,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -57,8 +61,10 @@ fun UpdateScreen(
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
onSelectAll(false)
|
||||
@@ -69,6 +75,8 @@ fun UpdateScreen(
|
||||
UpdatesAppBar(
|
||||
onCalendarClicked = { onCalendarClicked() },
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
onFilterClicked = { onFilterClicked() },
|
||||
hasFilters = hasActiveFilters,
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
@@ -139,6 +147,8 @@ fun UpdateScreen(
|
||||
private fun UpdatesAppBar(
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasFilters: Boolean,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
@@ -153,6 +163,12 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onFilterClicked,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_view_upcoming),
|
||||
icon = Icons.Outlined.CalendarMonth,
|
||||
|
||||
@@ -72,7 +72,7 @@ internal fun LazyListScope.updatesUiItems(
|
||||
// SY -->
|
||||
preserveReadingPosition: Boolean,
|
||||
// SY <--
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
@@ -120,11 +120,11 @@ internal fun LazyListScope.updatesUiItems(
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||
},
|
||||
onClick = {
|
||||
when {
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||
else -> onClickUpdate(updatesItem)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
import tachiyomi.i18n.MR
|
||||
import java.net.UnknownHostException
|
||||
|
||||
context(Context)
|
||||
context(context: Context)
|
||||
val Throwable.formattedMessage: String
|
||||
get() {
|
||||
when (this) {
|
||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
||||
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||
is UnknownHostException -> {
|
||||
return if (!isOnline()) {
|
||||
stringResource(MR.strings.exception_offline)
|
||||
return if (!context.isOnline()) {
|
||||
context.stringResource(MR.strings.exception_offline)
|
||||
} else {
|
||||
stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
context.stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
||||
is NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||
}
|
||||
return when (val className = this::class.simpleName) {
|
||||
"Exception", "IOException" -> message ?: className
|
||||
|
||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
// https://issuetracker.google.com/352584409
|
||||
context(LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
context(itemScope: LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ fun EhLoginWebViewScreen(
|
||||
)
|
||||
is LoadingState.Loading -> {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
(loadingState as? LoadingState.Loading)?.progress ?: 1f,
|
||||
loadingState.progress,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
label = "webview_loading",
|
||||
)
|
||||
|
||||
@@ -273,7 +273,7 @@ fun WebViewScreenContent(
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
is LoadingState.Loading -> LinearProgressIndicator(
|
||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
||||
progress = { loadingState.progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
@@ -247,6 +248,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
// SY <--
|
||||
}
|
||||
|
||||
memoryCache(
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context)
|
||||
.build(),
|
||||
)
|
||||
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
|
||||
@@ -10,6 +10,7 @@ import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.storage.service.StorageManager
|
||||
@@ -28,6 +29,7 @@ class DownloadProvider(
|
||||
private val context: Context,
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val downloadsDir: UniFile?
|
||||
@@ -190,6 +192,7 @@ class DownloadProvider(
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||
includeChapterUrlHash: Boolean = downloadPreferences.includeChapterUrlHash().get(),
|
||||
): String {
|
||||
var dirName = sanitizeChapterName(chapterName)
|
||||
if (!chapterScanlator.isNullOrBlank()) {
|
||||
@@ -197,7 +200,7 @@ class DownloadProvider(
|
||||
}
|
||||
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
|
||||
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
|
||||
dirName += "_" + md5(chapterUrl).take(6)
|
||||
if (includeChapterUrlHash) dirName += "_" + md5(chapterUrl).take(6)
|
||||
return dirName
|
||||
}
|
||||
|
||||
@@ -233,6 +236,7 @@ class DownloadProvider(
|
||||
chapterScanlator,
|
||||
chapterUrl,
|
||||
!libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||
!downloadPreferences.includeChapterUrlHash().get(),
|
||||
)
|
||||
|
||||
return buildList(2) {
|
||||
|
||||
@@ -420,7 +420,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
is SourceNotInstalledException -> context.stringResource(
|
||||
MR.strings.loader_not_implemented_error,
|
||||
)
|
||||
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
@@ -692,8 +691,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return File("")
|
||||
}
|
||||
|
||||
@@ -737,10 +735,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
const val KEY_GROUP_EXTRA = "group_extra"
|
||||
// SY <--
|
||||
|
||||
fun cancelAllWorks(context: Context) {
|
||||
context.workManager.cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(
|
||||
context: Context,
|
||||
prefInterval: Int? = null,
|
||||
@@ -754,16 +748,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
val networkRequest = NetworkRequest.Builder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
@@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
@@ -34,7 +40,26 @@ class SyncYomiSyncService(
|
||||
|
||||
private class SyncYomiException(message: String?) : Exception(message)
|
||||
|
||||
@Serializable
|
||||
private data class SyncEvent(
|
||||
val event: SyncEventStatus,
|
||||
@SerialName("device_name")
|
||||
val deviceName: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private enum class SyncEventStatus {
|
||||
SYNC_STARTED,
|
||||
SYNC_SUCCESS,
|
||||
SYNC_FAILED,
|
||||
SYNC_ERROR,
|
||||
SYNC_CANCELLED,
|
||||
}
|
||||
|
||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||
|
||||
try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
@@ -52,11 +77,23 @@ class SyncYomiSyncService(
|
||||
syncData
|
||||
}
|
||||
|
||||
pushSyncData(finalSyncData, etag)
|
||||
val success = pushSyncData(finalSyncData, etag)
|
||||
|
||||
if (success) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||
} else {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||
}
|
||||
|
||||
return finalSyncData.backup
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||
throw e
|
||||
}
|
||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||
notifier.showSyncError(e.message)
|
||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -123,8 +160,8 @@ class SyncYomiSyncService(
|
||||
/**
|
||||
* Return true if update success
|
||||
*/
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||
val backup = syncData.backup ?: return
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
||||
val backup = syncData.backup ?: return true
|
||||
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
@@ -163,13 +200,49 @@ class SyncYomiSyncService(
|
||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.lastSyncEtag().set(newETag)
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||
return true
|
||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||
// other clients updated remote data, will try next time
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||
return false
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) {
|
||||
withContext(NonCancellable) {
|
||||
try {
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
val url = "$host/api/sync/event"
|
||||
|
||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
val bodyObj = SyncEvent(
|
||||
event = event,
|
||||
deviceName = android.os.Build.MODEL,
|
||||
message = message,
|
||||
)
|
||||
|
||||
val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val request = POST(
|
||||
url = url,
|
||||
headers = headers,
|
||||
body = requestBody,
|
||||
)
|
||||
|
||||
val client = OkHttpClient()
|
||||
client.newCall(request).await().close()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -25,9 +24,6 @@ interface Tracker {
|
||||
|
||||
val supportsPrivateTracking: Boolean
|
||||
|
||||
@ColorInt
|
||||
fun getLogoColor(): Int
|
||||
|
||||
@DrawableRes
|
||||
fun getLogo(): Int
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -57,9 +56,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_anilist
|
||||
|
||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||
override fun getLogo() = R.drawable.brand_anilist
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||
|
||||
@@ -45,9 +45,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
||||
val query = $$"""
|
||||
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
@@ -82,14 +82,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
val query = $$"""
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|
||||
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|id: $listId, progress: $progress, status: $status, private: $private,
|
||||
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|
||||
|) {
|
||||
|id
|
||||
|status
|
||||
@@ -118,9 +118,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun deleteLibManga(track: DomainTrack) {
|
||||
withIOContext {
|
||||
val query = """
|
||||
|mutation DeleteManga(${'$'}listId: Int) {
|
||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||
val query = $$"""
|
||||
|mutation DeleteManga($listId: Int) {
|
||||
|DeleteMediaListEntry(id: $listId) {
|
||||
|deleted
|
||||
|}
|
||||
|}
|
||||
@@ -139,10 +139,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query Search(${'$'}query: String) {
|
||||
val query = $$"""
|
||||
|query Search($query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|id
|
||||
|staff {
|
||||
|edges {
|
||||
@@ -201,10 +201,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
val query = $$"""
|
||||
|query ($id: Int!, $manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|
||||
|id
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -84,9 +83,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_bangumi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(240, 145, 153)
|
||||
override fun getLogo() = R.drawable.brand_bangumi
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||
@@ -116,7 +113,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
// Users can set a 'username' (not nickname) once which effectively
|
||||
// replaces the stringified ID in certain queries.
|
||||
// If no username is set, the API returns the user ID as a strings
|
||||
var username = api.getUsername()
|
||||
val username = api.getUsername()
|
||||
saveCredentials(username, oauth.accessToken)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.kavita
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -34,9 +33,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
||||
|
||||
override fun getLogoColor() = Color.rgb(74, 198, 148)
|
||||
override fun getLogo(): Int = R.drawable.brand_kavita
|
||||
|
||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
@@ -140,7 +137,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
||||
}
|
||||
|
||||
authentication.apiUrl = prefApiUrl
|
||||
authentication.jwtToken = token.toString()
|
||||
authentication.jwtToken = token
|
||||
}
|
||||
authentications = oauth
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -37,9 +36,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
|
||||
private val api by lazy { KitsuApi(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_kitsu
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
override fun getLogo() = R.drawable.brand_kitsu
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -31,9 +30,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
|
||||
|
||||
val api by lazy { KomgaApi(id, client) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
override fun getLogo() = R.drawable.brand_komga
|
||||
|
||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.mangaupdates
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -44,9 +43,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
||||
|
||||
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
||||
|
||||
override fun getLogo(): Int = R.drawable.ic_manga_updates
|
||||
|
||||
override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
|
||||
override fun getLogo(): Int = R.drawable.brand_mangaupdates
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
|
||||
@@ -121,7 +118,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
val series = api.getSeries(track)
|
||||
return series?.let {
|
||||
return series.let {
|
||||
TrackMangaMetadata(
|
||||
it.seriesId,
|
||||
it.title?.htmlDecode(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.mdlist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -33,11 +32,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
val interceptor = MangaDexAuthInterceptor(trackPreferences, this)
|
||||
|
||||
override fun getLogo(): Int {
|
||||
return R.drawable.ic_tracker_mangadex_logo
|
||||
}
|
||||
|
||||
override fun getLogoColor(): Int {
|
||||
return Color.rgb(43, 48, 53)
|
||||
return R.drawable.brand_mangadex
|
||||
}
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
@@ -168,17 +163,17 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
trackPreferences.trackToken(this).delete()
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val manga = mdex.getMangaMetadata(track.toDbTrack())
|
||||
TrackMangaMetadata(
|
||||
remoteId = 0,
|
||||
title = manga?.title,
|
||||
thumbnailUrl = manga?.thumbnail_url, // Doesn't load the actual cover because of Refer header
|
||||
description = manga?.description,
|
||||
authors = manga?.author,
|
||||
artists = manga?.artist,
|
||||
title = manga.title,
|
||||
thumbnailUrl = manga.thumbnail_url, // Doesn't load the actual cover because of Refer header
|
||||
description = manga.description,
|
||||
authors = manga.author,
|
||||
artists = manga.artist,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -41,9 +40,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_mal
|
||||
|
||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||
override fun getLogo() = R.drawable.brand_myanimelist
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||
|
||||
@@ -12,15 +12,12 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
@@ -80,15 +77,15 @@ class MyAnimeListApi(
|
||||
// MAL API throws a 400 when the query is over 64 characters...
|
||||
.appendQueryParameter("q", query.take(64))
|
||||
.appendQueryParameter("nsfw", "true")
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALSearchResult>()
|
||||
.data
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.filter { !it.publishing_type.contains("novel") }
|
||||
.filter { !(it.node.mediaType.contains("novel")) }
|
||||
.map { parseSearchItem(it.node) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,29 +94,13 @@ class MyAnimeListApi(
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendPath(id.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
||||
)
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALManga>()
|
||||
.let {
|
||||
TrackSearch.create(trackId).apply {
|
||||
remote_id = it.id
|
||||
title = it.title
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
score = it.mean
|
||||
cover_url = it.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
start_date = it.startDate ?: ""
|
||||
}
|
||||
}
|
||||
.let { parseSearchItem(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,8 +164,7 @@ class MyAnimeListApi(
|
||||
|
||||
val matches = myListSearchResult.data
|
||||
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.map { parseSearchItem(it.node) }
|
||||
|
||||
// Check next page if there's more
|
||||
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
||||
@@ -216,12 +196,12 @@ class MyAnimeListApi(
|
||||
description = it.synopsis,
|
||||
authors = it.authors
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.mapNotNull { it.node.getFullName() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
artists = it.authors
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.mapNotNull { it.node.getFullName() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
@@ -230,10 +210,10 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
||||
private suspend fun getListPage(offset: Int): MALSearchResult {
|
||||
return withIOContext {
|
||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
|
||||
if (offset > 0) {
|
||||
urlBuilder.appendQueryParameter("offset", offset.toString())
|
||||
@@ -262,6 +242,28 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
|
||||
return TrackSearch.create(trackId).apply {
|
||||
remote_id = searchItem.id
|
||||
title = searchItem.title
|
||||
summary = searchItem.synopsis
|
||||
total_chapters = searchItem.numChapters
|
||||
score = searchItem.mean
|
||||
cover_url = searchItem.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = searchItem.status.replace("_", " ")
|
||||
publishing_type = searchItem.mediaType.replace("_", " ")
|
||||
start_date = searchItem.startDate ?: ""
|
||||
artists = searchItem.authors
|
||||
.filter { authorNode -> authorNode.role == "Art" }
|
||||
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||
authors = searchItem.authors
|
||||
// count all with "Story" or "Story & Art" as authors, like is done for library entries
|
||||
.filter { authorNode -> authorNode.role.contains("Story") }
|
||||
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(isoDate: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
@@ -273,7 +275,7 @@ class MyAnimeListApi(
|
||||
return try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(epochTime)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -284,6 +286,9 @@ class MyAnimeListApi(
|
||||
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
|
||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
||||
|
||||
private const val SEARCH_FIELDS =
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date,authors{first_name,last_name}"
|
||||
|
||||
private const val LIST_PAGINATION_AMOUNT = 250
|
||||
|
||||
private var codeVerifier: String = ""
|
||||
|
||||
@@ -18,8 +18,26 @@ data class MALManga(
|
||||
val mediaType: String,
|
||||
@SerialName("start_date")
|
||||
val startDate: String?,
|
||||
val authors: List<MALAuthorNode> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
val node: MALAuthor,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val id: Int,
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
) {
|
||||
fun getFullName(): String? = "$firstName $lastName".trim().ifBlank { null }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
@@ -33,19 +51,5 @@ data class MALMangaMetadata(
|
||||
val synopsis: String?,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val authors: List<MALAuthor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val node: MALAuthorNode,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
val authors: List<MALAuthorNode>,
|
||||
)
|
||||
|
||||
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class MALSearchResult(
|
||||
val data: List<MALSearchResultNode>,
|
||||
val paging: MALSearchPaging,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultNode(
|
||||
val node: MALSearchResultItem,
|
||||
val node: MALManga,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultItem(
|
||||
val id: Int,
|
||||
data class MALSearchPaging(
|
||||
val next: String?,
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchResult(
|
||||
val data: List<MALUserSearchItem>,
|
||||
val paging: MALUserSearchPaging,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchItem(
|
||||
val node: MALUserSearchItemNode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchPaging(
|
||||
val next: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALUserSearchItemNode(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -102,9 +101,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
override fun getLogo() = R.drawable.brand_shikimori
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -18,14 +17,15 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
||||
|
||||
val api by lazy { SuwayomiApi(id) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO
|
||||
override fun getLogo() = R.drawable.brand_suwayomi
|
||||
|
||||
companion object {
|
||||
const val UNREAD = 1L
|
||||
const val READING = 2L
|
||||
const val COMPLETED = 3L
|
||||
|
||||
private const val TRACKER_DELETE_KEY = "Tracker Delete"
|
||||
private const val TRACKER_DELETE_DEFAULT = false
|
||||
}
|
||||
|
||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||
@@ -58,7 +58,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
||||
}
|
||||
}
|
||||
|
||||
return api.updateProgress(track)
|
||||
return api.updateProgress(track, getPrefTrackerDelete())
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
@@ -105,4 +105,9 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
||||
|
||||
private fun String.getMangaId(): Long =
|
||||
this.substringAfterLast('/').toLong()
|
||||
|
||||
private fun getPrefTrackerDelete(): Boolean {
|
||||
val preferences = api.sourcePreferences()
|
||||
return preferences.getBoolean(TRACKER_DELETE_KEY, TRACKER_DELETE_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.addAll
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@@ -26,19 +29,22 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
|
||||
private val configurableSource: ConfigurableSource by lazy { (sourceManager.get(sourceId) as ConfigurableSource) }
|
||||
private val client: OkHttpClient by lazy { source.client }
|
||||
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
||||
|
||||
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
|
||||
|
||||
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||
val query = """
|
||||
|query GetManga(${'$'}mangaId: Int!) {
|
||||
| manga(id: ${'$'}mangaId) {
|
||||
val query = $$"""
|
||||
|query GetManga($mangaId: Int!) {
|
||||
| manga(id: $mangaId) {
|
||||
| ...MangaFragment
|
||||
| }
|
||||
|}
|
||||
|
|
||||
|$MangaFragment
|
||||
|$$MangaFragment
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
@@ -76,12 +82,14 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
suspend fun updateProgress(track: Track, deleteDownloadsOnServer: Boolean = false): Track {
|
||||
val mangaId = track.remote_id
|
||||
|
||||
val chaptersQuery = """
|
||||
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
||||
// TODO: Include a filter on the chapter number here
|
||||
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
|
||||
val chaptersQuery = $$"""
|
||||
|query GetMangaUnreadChapters($mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
|
||||
| nodes {
|
||||
| id
|
||||
| chapterNumber
|
||||
@@ -107,18 +115,29 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.data
|
||||
.entry
|
||||
.nodes
|
||||
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
|
||||
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read + 0.001 } }
|
||||
}
|
||||
|
||||
val markQuery = """
|
||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||
| chapters {
|
||||
| id
|
||||
| }
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
val markQuery = if (deleteDownloadsOnServer) {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
| deleteDownloadedChapters(input: {ids: $chapters}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
}
|
||||
val markPayload = buildJsonObject {
|
||||
put("query", markQuery)
|
||||
putJsonObject("variables") {
|
||||
@@ -137,12 +156,10 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val trackQuery = """
|
||||
|mutation TrackManga(${'$'}mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
||||
| trackRecords {
|
||||
| lastChapterRead
|
||||
| }
|
||||
val trackQuery = $$"""
|
||||
|mutation TrackManga($mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: $mangaId}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
|
||||
@@ -18,6 +18,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
|
||||
class PreferenceModule(val app: Application) : InjektModule {
|
||||
@@ -44,6 +45,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory {
|
||||
LibraryPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpdatesPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
ReaderPreferences(get())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
@SuppressLint("RequestInstallPackagesPolicy")
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
try {
|
||||
shellInterface?.install(
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
||||
)
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
|
||||
shellInterface?.install(it)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
continueQueue(InstallStep.Error)
|
||||
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun onDestroy() {
|
||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||
if (Shizuku.pingBinder()) {
|
||||
try {
|
||||
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) { "Failed to unbind shizuku service" }
|
||||
}
|
||||
}
|
||||
service.unregisterReceiver(receiver)
|
||||
logcat { "ShizukuInstaller destroy" }
|
||||
scope.cancel()
|
||||
|
||||
@@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
intent.data?.let { contentResolver.delete(it, null, null) }
|
||||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
|
||||
@@ -1,66 +1,48 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
*/
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
|
||||
/**
|
||||
* The broadcast receiver which listens to download completion events.
|
||||
*/
|
||||
private val downloadReceiver = DownloadCompletionReceiver()
|
||||
|
||||
/**
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||
internal class ExtensionInstaller(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val activeJobs = mutableMapOf<String, Job>()
|
||||
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
|
||||
private val httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
|
||||
val pkgName = extension.pkgName
|
||||
val downloadId = extension.pkgName.hashCode().toLong()
|
||||
cancelInstall(extension.pkgName)
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
val step = MutableStateFlow(InstallStep.Pending)
|
||||
activeSteps[downloadId] = step
|
||||
|
||||
// Register the receiver after removing (and unregistering) the previous download
|
||||
downloadReceiver.register()
|
||||
val job = scope.launch {
|
||||
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
|
||||
try {
|
||||
step.value = InstallStep.Downloading
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
val downloadUri = url.toUri()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Failed to download extension")
|
||||
}
|
||||
response.body.byteStream().use { input ->
|
||||
tmpFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
||||
downloadsStateFlows[id] = downloadStateFlow
|
||||
|
||||
// Poll download status
|
||||
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
|
||||
// Map to our model
|
||||
when (downloadStatus) {
|
||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||
else -> null
|
||||
step.value = InstallStep.Installing
|
||||
installApk(downloadId, tmpFile)
|
||||
} catch (e: Exception) {
|
||||
if (e is InterruptedException) {
|
||||
// Canceled
|
||||
} else {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
step.value = InstallStep.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
|
||||
emit(it)
|
||||
// Stop when the application is installed or errors
|
||||
!it.isCompleted()
|
||||
}.onCompletion {
|
||||
// Always notify on main thread
|
||||
withUIContext {
|
||||
// Always remove the download when unsubscribed
|
||||
deleteDownload(pkgName)
|
||||
activeJobs[extension.pkgName] = job
|
||||
|
||||
return step.asStateFlow()
|
||||
.onCompletion {
|
||||
activeJobs.remove(extension.pkgName)
|
||||
activeSteps.remove(downloadId)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow that polls the given download id for its status every second, as the
|
||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||
*
|
||||
* @param id The id of the download to poll.
|
||||
*/
|
||||
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
while (true) {
|
||||
// Get the current download status
|
||||
val downloadStatus = downloadManager.query(query).use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@flow
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||
}
|
||||
|
||||
emit(downloadStatus)
|
||||
|
||||
// Stop polling when the download fails or finishes
|
||||
if (
|
||||
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
||||
downloadStatus == DownloadManager.STATUS_FAILED
|
||||
) {
|
||||
return@flow
|
||||
}
|
||||
|
||||
delay(1.seconds)
|
||||
}
|
||||
}
|
||||
// Ignore duplicate results
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Starts an intent to install the extension at the given uri.
|
||||
*
|
||||
* @param uri The uri of the extension to install.
|
||||
* @param tempFile The file of the extension to install. Delete after use.
|
||||
*/
|
||||
fun installApk(downloadId: Long, uri: Uri) {
|
||||
private fun installApk(downloadId: Long, tempFile: File) {
|
||||
when (val installer = extensionInstaller.get()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
||||
|
||||
if (tempFile.exists() && !tempFile.delete()) {
|
||||
// Unlikely but just in case
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
||||
updateInstallStep(downloadId, InstallStep.Installed)
|
||||
} else {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
|
||||
tempFile.delete()
|
||||
}
|
||||
else -> {
|
||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
val intent = ExtensionInstallService.getIntent(
|
||||
context,
|
||||
downloadId,
|
||||
tempFile.getUriCompat(context),
|
||||
installer,
|
||||
)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -201,9 +140,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* Cancels extension install and remove from download manager and installer.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
Installer.cancelInstallQueue(context, downloadId)
|
||||
activeJobs.remove(pkgName)?.cancel()
|
||||
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param step New install step.
|
||||
*/
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
downloadsStateFlows[downloadId]?.let { it.value = step }
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the download for the given package name.
|
||||
*
|
||||
* @param pkgName The package name of the download to delete.
|
||||
*/
|
||||
private fun deleteDownload(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName)
|
||||
if (downloadId != null) {
|
||||
downloadManager.remove(downloadId)
|
||||
downloadsStateFlows.remove(downloadId)
|
||||
}
|
||||
if (activeDownloads.isEmpty()) {
|
||||
downloadReceiver.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver that listens to download status events.
|
||||
*/
|
||||
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Whether this receiver is currently registered.
|
||||
*/
|
||||
private var isRegistered = false
|
||||
|
||||
/**
|
||||
* Registers this receiver if it's not already.
|
||||
*/
|
||||
fun register() {
|
||||
if (isRegistered) return
|
||||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this receiver if it's not already.
|
||||
*/
|
||||
fun unregister() {
|
||||
if (!isRegistered) return
|
||||
isRegistered = false
|
||||
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download event is received. It looks for the download in the current active
|
||||
* downloads and notifies its installation step.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
|
||||
|
||||
// Avoid events for downloads we didn't request
|
||||
if (id !in activeDownloads.values) return
|
||||
|
||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
// Set next installation step
|
||||
if (uri == null) {
|
||||
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
||||
updateInstallStep(id, InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val localUri = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
||||
).removePrefix(FILE_SCHEME)
|
||||
|
||||
installApk(id, File(localUri).getUriCompat(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
activeSteps[downloadId]?.let { it.value = step }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
private fun coverQuality() = sourcePreferences.getString(getCoverQualityPrefKey(mdLang.lang), "").orEmpty()
|
||||
private fun tryUsingFirstVolumeCover() = sourcePreferences.getBoolean(getTryUsingFirstVolumeCoverKey(mdLang.lang), false)
|
||||
private fun altTitlesInDesc() = sourcePreferences.getBoolean(getAltTitlesInDescKey(mdLang.lang), false)
|
||||
private fun finalChapterInDesc() = sourcePreferences.getBoolean(getFinalChapterInDescPrefKey(mdLang.lang), false)
|
||||
private fun preferExtensionLangTitle() = sourcePreferences.getBoolean(getPreferExtensionLangTitlePrefKey(mdLang.extLang), true)
|
||||
|
||||
private val mangadexService by lazy {
|
||||
MangaDexService(client)
|
||||
@@ -107,7 +109,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
FollowsHandler(mdLang.lang, mangadexAuthService)
|
||||
}
|
||||
private val mangaHandler by lazy {
|
||||
MangaHandler(mdLang.lang, mangadexService, apiMangaParser, followsHandler)
|
||||
MangaHandler(mdLang.lang, mangadexService, apiMangaParser)
|
||||
}
|
||||
private val similarHandler by lazy {
|
||||
SimilarHandler(mdLang.lang, mangadexService, similarService)
|
||||
@@ -192,11 +194,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return mangaHandler.fetchMangaDetailsObservable(manga, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||
return mangaHandler.fetchMangaDetailsObservable(
|
||||
manga,
|
||||
id,
|
||||
coverQuality(),
|
||||
tryUsingFirstVolumeCover(),
|
||||
altTitlesInDesc(),
|
||||
finalChapterInDesc(),
|
||||
preferExtensionLangTitle(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return mangaHandler.getMangaDetails(manga, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||
return mangaHandler.getMangaDetails(
|
||||
manga,
|
||||
id,
|
||||
coverQuality(),
|
||||
tryUsingFirstVolumeCover(),
|
||||
altTitlesInDesc(),
|
||||
finalChapterInDesc(),
|
||||
preferExtensionLangTitle(),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
@@ -241,8 +259,21 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
|
||||
override fun newMetaInstance() = MangaDexSearchMetadata()
|
||||
|
||||
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) {
|
||||
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third, null, coverQuality(), altTitlesInDesc())
|
||||
override suspend fun parseIntoMetadata(
|
||||
metadata: MangaDexSearchMetadata,
|
||||
input: Triple<MangaDto, List<String>, StatisticsMangaDto>,
|
||||
) {
|
||||
apiMangaParser.parseIntoMetadata(
|
||||
metadata,
|
||||
input.first,
|
||||
input.second,
|
||||
input.third,
|
||||
null,
|
||||
coverQuality(),
|
||||
altTitlesInDesc(),
|
||||
finalChapterInDesc(),
|
||||
preferExtensionLangTitle(),
|
||||
)
|
||||
}
|
||||
|
||||
// LoginSource methods
|
||||
@@ -296,10 +327,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return followsHandler.updateRating(track)
|
||||
}
|
||||
|
||||
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> {
|
||||
return mangaHandler.getTrackingInfo(track)
|
||||
}
|
||||
|
||||
// RandomMangaSource method
|
||||
override suspend fun fetchRandomMangaUrl(): String {
|
||||
return mangaHandler.fetchRandomMangaId()
|
||||
@@ -313,51 +340,62 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return similarHandler.getRelated(manga)
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: Track): SManga? {
|
||||
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||
suspend fun getMangaMetadata(track: Track): SManga {
|
||||
return mangaHandler.getMangaMetadata(
|
||||
track,
|
||||
id,
|
||||
coverQuality(),
|
||||
tryUsingFirstVolumeCover(),
|
||||
altTitlesInDesc(),
|
||||
finalChapterInDesc(),
|
||||
preferExtensionLangTitle(),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val dataSaverPref = "dataSaverV5"
|
||||
|
||||
fun getDataSaverPreferenceKey(dexLang: String): String {
|
||||
return "${dataSaverPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val standardHttpsPortPref = "usePort443"
|
||||
|
||||
fun getStandardHttpsPreferenceKey(dexLang: String): String {
|
||||
return "${standardHttpsPortPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val blockedGroupsPref = "blockedGroups"
|
||||
|
||||
fun getBlockedGroupsPrefKey(dexLang: String): String {
|
||||
return "${blockedGroupsPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val blockedUploaderPref = "blockedUploader"
|
||||
|
||||
fun getBlockedUploaderPrefKey(dexLang: String): String {
|
||||
return "${blockedUploaderPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val coverQualityPref = "thumbnailQuality"
|
||||
|
||||
fun getCoverQualityPrefKey(dexLang: String): String {
|
||||
return "${coverQualityPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val tryUsingFirstVolumeCover = "tryUsingFirstVolumeCover"
|
||||
|
||||
private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
|
||||
fun getTryUsingFirstVolumeCoverKey(dexLang: String): String {
|
||||
return "${tryUsingFirstVolumeCover}_$dexLang"
|
||||
return "${tryUsingFirstVolumeCoverPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val altTitlesInDesc = "altTitlesInDesc"
|
||||
|
||||
private const val altTitlesInDescPref = "altTitlesInDesc"
|
||||
fun getAltTitlesInDescKey(dexLang: String): String {
|
||||
return "${altTitlesInDesc}_$dexLang"
|
||||
return "${altTitlesInDescPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val finalChapterInDescPref = "finalChapterInDesc"
|
||||
fun getFinalChapterInDescPrefKey(dexLang: String): String {
|
||||
return "${finalChapterInDescPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val preferExtensionLangTitlePref = "preferExtensionLangTitle"
|
||||
fun getPreferExtensionLangTitlePrefKey(dexLang: String): String {
|
||||
return "${preferExtensionLangTitlePref}_$dexLang"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-63
@@ -50,79 +50,46 @@ class ExtensionsScreenModel(
|
||||
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
val queryFilter: (String) -> ((Extension) -> Boolean) = { query ->
|
||||
filter@{ extension ->
|
||||
if (query.isEmpty()) return@filter true
|
||||
query.split(",").any { _input ->
|
||||
val input = _input.trim()
|
||||
if (input.isEmpty()) return@any false
|
||||
when (extension) {
|
||||
is Extension.Available -> {
|
||||
extension.sources.any {
|
||||
it.name.contains(input, ignoreCase = true) ||
|
||||
it.baseUrl.contains(input, ignoreCase = true) ||
|
||||
it.id == input.toLongOrNull()
|
||||
} ||
|
||||
extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
is Extension.Installed -> {
|
||||
extension.sources.any {
|
||||
it.name.contains(input, ignoreCase = true) ||
|
||||
it.id == input.toLongOrNull() ||
|
||||
if (it is HttpSource) {
|
||||
it.baseUrl.contains(input, ignoreCase = true)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} ||
|
||||
extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenModelScope.launchIO {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
||||
state.map { it.searchQuery }
|
||||
.distinctUntilChanged()
|
||||
.debounce(SEARCH_DEBOUNCE_MILLIS)
|
||||
.map { searchQueryPredicate(it ?: "") },
|
||||
currentDownloads,
|
||||
getExtensions.subscribe(),
|
||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
val searchQuery = query ?: ""
|
||||
|
||||
val itemsGroups: ItemGroups = mutableMapOf()
|
||||
|
||||
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates
|
||||
}
|
||||
|
||||
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = installed + untrusted
|
||||
}
|
||||
|
||||
val languagesWithExtensions = _available
|
||||
.filter(queryFilter(searchQuery))
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(LocaleHelper.comparator)
|
||||
.map { (lang, exts) ->
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||
exts.map(extensionMapper(downloads))
|
||||
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
buildMap {
|
||||
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||
}
|
||||
if (languagesWithExtensions.isNotEmpty()) {
|
||||
itemsGroups.putAll(languagesWithExtensions)
|
||||
}
|
||||
|
||||
itemsGroups
|
||||
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
|
||||
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
|
||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
|
||||
}
|
||||
|
||||
val languagesWithExtensions = _available
|
||||
.filter(predicate)
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(LocaleHelper.comparator)
|
||||
.map { (lang, exts) ->
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||
exts.map(extensionMapper(downloads))
|
||||
}
|
||||
if (languagesWithExtensions.isNotEmpty()) {
|
||||
putAll(languagesWithExtensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
.collectLatest {
|
||||
.collectLatest { items ->
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
items = it,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
|
||||
val subqueries = query.split(",")
|
||||
.map { it.trim() }
|
||||
.filterNot { it.isBlank() }
|
||||
|
||||
if (subqueries.isEmpty()) return { true }
|
||||
|
||||
return { extension ->
|
||||
subqueries.any { subquery ->
|
||||
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
|
||||
|
||||
when (extension) {
|
||||
is Extension.Installed -> extension.sources.any { source ->
|
||||
source.name.contains(subquery, ignoreCase = true) ||
|
||||
(source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true ||
|
||||
source.id == subquery.toLongOrNull()
|
||||
}
|
||||
|
||||
is Extension.Available -> extension.sources.any {
|
||||
it.name.contains(subquery, ignoreCase = true) ||
|
||||
it.baseUrl.contains(subquery, ignoreCase = true) ||
|
||||
it.id == subquery.toLongOrNull()
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
mutableState.update {
|
||||
it.copy(searchQuery = query)
|
||||
@@ -222,7 +219,7 @@ class ExtensionsScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
typealias ItemGroups = MutableMap<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
||||
typealias ItemGroups = Map<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
||||
|
||||
object ExtensionUiModel {
|
||||
sealed interface Header {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -49,6 +50,10 @@ fun extensionsTab(
|
||||
),
|
||||
),
|
||||
content = { contentPadding, _ ->
|
||||
BackHandler(enabled = state.searchQuery != null) {
|
||||
extensionsScreenModel.search(null)
|
||||
}
|
||||
|
||||
ExtensionScreen(
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
|
||||
+19
-15
@@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@@ -29,7 +32,6 @@ import mihon.feature.migration.config.MigrationConfigScreen
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (state.selectionMode) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.selectionMode,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
|
||||
+13
-9
@@ -1,17 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@@ -35,7 +38,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
@@ -74,13 +76,15 @@ data class MigrateSourceSearchScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.filters.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -11,13 +11,13 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.InputChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -155,7 +155,7 @@ fun AutoCompleteTextField(
|
||||
null
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryEditable)
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable)
|
||||
.fillMaxWidth()
|
||||
.runOnEnterKeyPressed { submit() },
|
||||
singleLine = true,
|
||||
@@ -190,7 +190,7 @@ fun AutoCompleteTextField(
|
||||
if (value.text.length > 2 && filteredValues.isNotEmpty()) {
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier
|
||||
.exposedDropdownSize(matchTextFieldWidth = true),
|
||||
.exposedDropdownSize(matchAnchorWidth = true),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.download
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -16,8 +13,10 @@ import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Pill
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
)
|
||||
}
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (downloadList.isEmpty()) {
|
||||
|
||||
@@ -84,6 +84,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
@@ -122,6 +123,7 @@ class LibraryScreenModel(
|
||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
@@ -404,9 +406,7 @@ class LibraryScreenModel(
|
||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val mangaTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
||||
.orEmpty()
|
||||
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
||||
@@ -736,15 +736,19 @@ class LibraryScreenModel(
|
||||
* Queues the amount specified of unread chapters from the list of selected manga
|
||||
*/
|
||||
fun performDownloadAction(action: DownloadAction) {
|
||||
val mangas = state.value.selectedManga
|
||||
val amount = when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
||||
DownloadAction.UNREAD_CHAPTERS -> null
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
private fun downloadNextChapters(amount: Int?) {
|
||||
val mangas = state.value.selectedManga
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
// SY -->
|
||||
@@ -794,6 +798,54 @@ class LibraryScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadBookmarkedChapters() {
|
||||
val mangas = state.value.selectedManga
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
// SY -->
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
val mergedMangas = getMergedMangaById.await(manga.id)
|
||||
.associateBy { it.id }
|
||||
getBookmarkedChaptersByMangaId.await(manga.id)
|
||||
.groupBy { it.mangaId }
|
||||
.forEach ab@{ (mangaId, chapters) ->
|
||||
val mergedManga = mergedMangas[mangaId] ?: return@ab
|
||||
val downloadChapters = chapters.fastFilterNot { chapter ->
|
||||
downloadManager.queueState.value.fastAny { chapter.id == it.chapter.id } ||
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
mergedManga.ogTitle,
|
||||
mergedManga.source,
|
||||
)
|
||||
}
|
||||
|
||||
downloadManager.downloadChapters(mergedManga, downloadChapters)
|
||||
}
|
||||
|
||||
return@forEach
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val chapters = getBookmarkedChaptersByMangaId.await(manga.id)
|
||||
.fastFilterNot { chapter ->
|
||||
downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
// SY -->
|
||||
manga.ogTitle,
|
||||
// SY <--
|
||||
manga.source,
|
||||
)
|
||||
}
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun cleanTitles() {
|
||||
state.value.selectedManga.fastFilter {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
@@ -208,7 +207,9 @@ class MangaScreen(
|
||||
previewsRowCount = successState.previewsRowCount,
|
||||
onMigrateClicked = {
|
||||
navigator.push(MigrationConfigScreen(successState.manga.id))
|
||||
}.takeIf { successState.manga.favorite },
|
||||
}.takeIf {
|
||||
successState.manga.favorite /* SY --> */ && successState.manga.source != MERGED_SOURCE_ID /* SY <-- */
|
||||
},
|
||||
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||
// SY -->
|
||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||
@@ -403,12 +404,7 @@ class MangaScreen(
|
||||
try {
|
||||
getMangaUrl(manga_, source_)?.let { url ->
|
||||
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
@@ -1175,6 +1175,13 @@ class MangaScreenModel(
|
||||
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
|
||||
}
|
||||
|
||||
private fun getBookmarkedChapters(): List<Chapter> {
|
||||
val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty()
|
||||
return chapterItems
|
||||
.filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED }
|
||||
.map { it.chapter }
|
||||
}
|
||||
|
||||
private fun startDownload(
|
||||
chapters: List<Chapter>,
|
||||
startNow: Boolean,
|
||||
@@ -1237,6 +1244,7 @@ class MangaScreenModel(
|
||||
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
|
||||
}
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
startDownload(chaptersToDownload, false)
|
||||
@@ -1487,7 +1495,6 @@ class MangaScreenModel(
|
||||
fun toggleSelection(
|
||||
item: ChapterList.Item,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
updateSuccessState { successState ->
|
||||
@@ -1502,7 +1509,7 @@ class MangaScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.id, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -1528,7 +1535,7 @@ class MangaScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
|
||||
@@ -467,7 +467,9 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.flushReadTimer()
|
||||
lifecycleScope.launchNonCancellable {
|
||||
viewModel.updateHistory()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -932,7 +934,7 @@ class ReaderActivity : BaseActivity() {
|
||||
private fun shareChapter() {
|
||||
assistUrl?.let {
|
||||
val intent = it.toUri().toShareIntent(this, type = "text/plain")
|
||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,7 +1139,7 @@ class ReaderActivity : BaseActivity() {
|
||||
context = applicationContext,
|
||||
message = /* SY --> */ text, // SY <--
|
||||
)
|
||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun onCopyImageResult(uri: Uri) {
|
||||
|
||||
@@ -487,7 +487,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
viewModelScope.launchIO {
|
||||
logcat { "Loading ${chapter.chapter.url}" }
|
||||
|
||||
flushReadTimer()
|
||||
updateHistory()
|
||||
restartReadTimer()
|
||||
|
||||
try {
|
||||
@@ -655,7 +655,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
* if setting is enabled and [currentChapter] is queued for download
|
||||
*/
|
||||
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
|
||||
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also {
|
||||
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!)?.also {
|
||||
downloadManager.cancelQueuedDownloads(listOf(it))
|
||||
}
|
||||
}
|
||||
@@ -767,40 +767,37 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
chapter.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
|
||||
) {
|
||||
ChapterUpdate(id = chapter.id, read = true)
|
||||
// SY -->
|
||||
.also { deleteChapterIfNeeded(ReaderChapter(chapter)) }
|
||||
// SY <--
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
updateChapter.awaitAll(duplicateUnreadChapters)
|
||||
// SY -->
|
||||
duplicateUnreadChapters.forEach { chapterUpdate ->
|
||||
val chapter = unfilteredChapterList.first { it.id == chapterUpdate.id }
|
||||
deleteChapterIfNeeded(ReaderChapter(chapter))
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
fun restartReadTimer() {
|
||||
chapterReadStartTime = Instant.now().toEpochMilli()
|
||||
}
|
||||
|
||||
fun flushReadTimer() {
|
||||
getCurrentChapter()?.let {
|
||||
viewModelScope.launchNonCancellable {
|
||||
updateHistory(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the chapter last read history if incognito mode isn't on.
|
||||
*/
|
||||
private suspend fun updateHistory(readerChapter: ReaderChapter) {
|
||||
if (incognitoMode) return
|
||||
suspend fun updateHistory() {
|
||||
getCurrentChapter()?.let { readerChapter ->
|
||||
if (incognitoMode) return@let
|
||||
|
||||
val chapterId = readerChapter.chapter.id!!
|
||||
val endTime = Date()
|
||||
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
||||
val chapterId = readerChapter.chapter.id!!
|
||||
val endTime = Date()
|
||||
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
||||
|
||||
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
||||
chapterReadStartTime = null
|
||||
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
||||
chapterReadStartTime = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,7 +848,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
viewModelScope.launchNonCancellable {
|
||||
updateChapter.await(
|
||||
ChapterUpdate(
|
||||
id = chapter.id!!.toLong(),
|
||||
id = chapter.id!!,
|
||||
bookmark = bookmarked,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -69,15 +69,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
||||
// selected as the current chapter when one of those pages is selected.
|
||||
val prevPages = chapters.prevChapter.pages
|
||||
if (prevPages != null) {
|
||||
newItems.addAll(prevPages.takeLast(2))
|
||||
}
|
||||
}
|
||||
// Add previous chapter pages and transition
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
@@ -119,14 +112,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// Add at most two pages, because this chapter will be selected before the user can
|
||||
// swap more pages.
|
||||
val nextPages = chapters.nextChapter.pages
|
||||
if (nextPages != null) {
|
||||
newItems.addAll(nextPages.take(2))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Resets double-page splits, else insert pages get misplaced
|
||||
subItems.filterIsInstance<InsertPage>().also { subItems.removeAll(it) }
|
||||
@@ -146,7 +132,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
|
||||
// Will skip insert page otherwise
|
||||
if (insertPageLastPage != null) {
|
||||
viewer.moveToPage(insertPageLastPage!!)
|
||||
viewer.moveToPage(insertPageLastPage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,14 +43,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
||||
// selected as the current chapter when one of those pages is selected.
|
||||
val prevPages = chapters.prevChapter.pages
|
||||
if (prevPages != null) {
|
||||
newItems.addAll(prevPages.takeLast(2))
|
||||
}
|
||||
}
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
@@ -70,14 +63,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// Add at most two pages, because this chapter will be selected before the user can
|
||||
// swap more pages.
|
||||
val nextPages = chapters.nextChapter.pages
|
||||
if (nextPages != null) {
|
||||
newItems.addAll(nextPages.take(2))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
updateItems(newItems)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
||||
|
||||
internal val trackerManager: TrackerManager by injectLazy()
|
||||
|
||||
abstract fun handleResult(data: Uri?)
|
||||
abstract fun handleResult(uri: Uri)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -23,7 +23,12 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
||||
LoadingScreen()
|
||||
}
|
||||
|
||||
handleResult(intent.data)
|
||||
val data = intent.data
|
||||
if (data == null) {
|
||||
returnToSettings()
|
||||
} else {
|
||||
handleResult(data)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun returnToSettings() {
|
||||
|
||||
@@ -12,9 +12,9 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
class GoogleDriveLoginActivity : BaseOAuthLoginActivity() {
|
||||
private val googleDriveService = Injekt.get<GoogleDriveService>()
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
val error = data?.getQueryParameter("error")
|
||||
override fun handleResult(uri: Uri) {
|
||||
val code = uri.getQueryParameter("code")
|
||||
val error = uri.getQueryParameter("error")
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
googleDriveService.handleAuthorizationCode(
|
||||
|
||||
@@ -2,69 +2,64 @@ package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TrackLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
override fun handleResult(data: Uri?) {
|
||||
when (data?.host) {
|
||||
"anilist-auth" -> handleAnilist(data)
|
||||
"bangumi-auth" -> handleBangumi(data)
|
||||
"myanimelist-auth" -> handleMyAnimeList(data)
|
||||
"shikimori-auth" -> handleShikimori(data)
|
||||
override fun handleResult(uri: Uri) {
|
||||
val data = when {
|
||||
!uri.encodedQuery.isNullOrBlank() -> uri.encodedQuery
|
||||
!uri.encodedFragment.isNullOrBlank() -> uri.encodedFragment
|
||||
else -> null
|
||||
}
|
||||
?.split("&")
|
||||
?.filter { it.isNotBlank() }
|
||||
?.associate {
|
||||
val parts = it.split("=", limit = 2).map<String, String>(Uri::decode)
|
||||
parts[0] to parts.getOrNull(1)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
lifecycleScope.launch {
|
||||
when (uri.host) {
|
||||
"anilist-auth" -> handleAniList(data["access_token"])
|
||||
"bangumi-auth" -> handleBangumi(data["code"])
|
||||
"myanimelist-auth" -> handleMyAnimeList(data["code"])
|
||||
"shikimori-auth" -> handleShikimori(data["code"])
|
||||
}
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAnilist(data: Uri) {
|
||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||
val matchResult = regex.find(data.fragment.toString())
|
||||
if (matchResult?.groups?.get(1) != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
returnToSettings()
|
||||
}
|
||||
private suspend fun handleAniList(accessToken: String?) {
|
||||
if (accessToken != null) {
|
||||
trackerManager.aniList.login(accessToken)
|
||||
} else {
|
||||
trackerManager.aniList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBangumi(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleBangumi(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.bangumi.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.bangumi.login(code)
|
||||
} else {
|
||||
trackerManager.bangumi.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMyAnimeList(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleMyAnimeList(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.myAnimeList.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.myAnimeList.login(code)
|
||||
} else {
|
||||
trackerManager.myAnimeList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShikimori(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleShikimori(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.shikimori.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.shikimori.login(code)
|
||||
} else {
|
||||
trackerManager.shikimori.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.preference.asState
|
||||
@@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -43,9 +49,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.model.applyFilter
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
@@ -60,6 +68,7 @@ class UpdatesScreenModel(
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
// SY -->
|
||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
@@ -85,19 +94,35 @@ class UpdatesScreenModel(
|
||||
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
||||
|
||||
combine(
|
||||
getUpdates.subscribe(limit).distinctUntilChanged(),
|
||||
// needed for SQL filters (unread, started, bookmarked, etc)
|
||||
getUpdatesItemPreferenceFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
getUpdates.subscribe(
|
||||
limit,
|
||||
unread = it.filterUnread.toBooleanOrNull(),
|
||||
started = it.filterStarted.toBooleanOrNull(),
|
||||
bookmarked = it.filterBookmarked.toBooleanOrNull(),
|
||||
hideExcludedScanlators = it.filterExcludedScanlators,
|
||||
).distinctUntilChanged()
|
||||
},
|
||||
downloadCache.changes,
|
||||
downloadManager.queueState,
|
||||
) { updates, _, _ -> updates }
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
// needed for Kotlin filters (downloaded)
|
||||
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
|
||||
old.filterDownloaded == new.filterDownloaded
|
||||
},
|
||||
) { updates, _, _, itemPreferences ->
|
||||
updates
|
||||
.toUpdateItems()
|
||||
.applyFilters(itemPreferences)
|
||||
.toPersistentList()
|
||||
}
|
||||
.collectLatest { updateItems ->
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
items = updates.toUpdateItems(),
|
||||
items = updateItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,9 +133,43 @@ class UpdatesScreenModel(
|
||||
.catch { logcat(LogPriority.ERROR, it) }
|
||||
.collect(this@UpdatesScreenModel::updateDownloadState)
|
||||
}
|
||||
|
||||
getUpdatesItemPreferenceFlow()
|
||||
.map { prefs ->
|
||||
listOf(
|
||||
prefs.filterUnread,
|
||||
prefs.filterDownloaded,
|
||||
prefs.filterStarted,
|
||||
prefs.filterBookmarked,
|
||||
)
|
||||
.any { it != TriState.DISABLED }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
mutableState.update { state ->
|
||||
state.copy(hasActiveFilters = it)
|
||||
}
|
||||
}
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateItems(): PersistentList<UpdatesItem> {
|
||||
private fun List<UpdatesItem>.applyFilters(
|
||||
preferences: ItemPreferences,
|
||||
): List<UpdatesItem> {
|
||||
val filterDownloaded = preferences.filterDownloaded
|
||||
|
||||
val filterFnDownloaded: (UpdatesItem) -> Boolean = {
|
||||
applyFilter(filterDownloaded) {
|
||||
it.downloadStateProvider() == Download.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
||||
return fastFilter {
|
||||
filterFnDownloaded(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
||||
return this
|
||||
.map { update ->
|
||||
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
||||
@@ -135,7 +194,6 @@ class UpdatesScreenModel(
|
||||
selected = update.chapterId in selectedChapterIds,
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
fun updateLibrary(): Boolean {
|
||||
@@ -279,7 +337,6 @@ class UpdatesScreenModel(
|
||||
fun toggleSelection(
|
||||
item: UpdatesItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
mutableState.update { state ->
|
||||
@@ -294,7 +351,7 @@ class UpdatesScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -320,7 +377,7 @@ class UpdatesScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
@@ -373,9 +430,41 @@ class UpdatesScreenModel(
|
||||
libraryPreferences.newUpdatesCount().set(0)
|
||||
}
|
||||
|
||||
private fun getUpdatesItemPreferenceFlow(): Flow<ItemPreferences> {
|
||||
return combine(
|
||||
updatesPreferences.filterDownloaded().changes(),
|
||||
updatesPreferences.filterUnread().changes(),
|
||||
updatesPreferences.filterStarted().changes(),
|
||||
updatesPreferences.filterBookmarked().changes(),
|
||||
updatesPreferences.filterExcludedScanlators().changes(),
|
||||
) { downloaded, unread, started, bookmarked, excludedScanlators ->
|
||||
ItemPreferences(
|
||||
filterDownloaded = downloaded,
|
||||
filterUnread = unread,
|
||||
filterStarted = started,
|
||||
filterBookmarked = bookmarked,
|
||||
filterExcludedScanlators = excludedScanlators,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showFilterDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class ItemPreferences(
|
||||
val filterDownloaded: TriState,
|
||||
val filterUnread: TriState,
|
||||
val filterStarted: TriState,
|
||||
val filterBookmarked: TriState,
|
||||
val filterExcludedScanlators: Boolean,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
@@ -399,6 +488,7 @@ class UpdatesScreenModel(
|
||||
|
||||
sealed interface Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
||||
data object FilterSheet : Dialog
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
@@ -407,6 +497,14 @@ class UpdatesScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun TriState.toBooleanOrNull(): Boolean? {
|
||||
return when (this) {
|
||||
TriState.DISABLED -> null
|
||||
TriState.ENABLED_IS -> true
|
||||
TriState.ENABLED_NOT -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class UpdatesSettingsScreenModel(
|
||||
val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
) : ScreenModel {
|
||||
|
||||
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
|
||||
preference(updatesPreferences).getAndSet {
|
||||
it.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||
import eu.kanade.presentation.updates.UpdatesFilterDialog
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||
@@ -70,6 +71,7 @@ data object UpdatesTab : Tab {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
UpdateScreen(
|
||||
@@ -93,6 +95,8 @@ data object UpdatesTab : Tab {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||
onFilterClicked = screenModel::showFilterDialog,
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
)
|
||||
|
||||
val onDismissDialog = { screenModel.setDialog(null) }
|
||||
@@ -103,6 +107,12 @@ data object UpdatesTab : Tab {
|
||||
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||
)
|
||||
}
|
||||
is UpdatesScreenModel.Dialog.FilterSheet -> {
|
||||
UpdatesFilterDialog(
|
||||
onDismissRequest = onDismissDialog,
|
||||
screenModel = settingsScreenModel,
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class CrashLogUtil(
|
||||
|
||||
suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext {
|
||||
try {
|
||||
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
|
||||
val file = context.createFileInCacheDir("tachiyomi_sy_crash_logs.txt")
|
||||
|
||||
file.appendText(getDebugInfo() + "\n\n")
|
||||
getExtensionsInfo()?.let { file.appendText("$it\n\n") }
|
||||
|
||||
@@ -5,6 +5,7 @@ package androidx.preference
|
||||
/**
|
||||
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||
*/
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||
return onBindEditTextListener
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.test
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
@@ -20,8 +19,7 @@ data class DummyTracker(
|
||||
override val supportsPrivateTracking: Boolean = false,
|
||||
override val isLoggedIn: Boolean = false,
|
||||
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
||||
val valLogoColor: Int = Color.rgb(18, 25, 35),
|
||||
val valLogo: Int = R.drawable.ic_tracker_anilist,
|
||||
val valLogo: Int = R.drawable.brand_anilist,
|
||||
val valStatuses: List<Long> = (1L..6L).toList(),
|
||||
val valReadingStatus: Long = 1L,
|
||||
val valRereadingStatus: Long = 1L,
|
||||
@@ -34,8 +32,6 @@ data class DummyTracker(
|
||||
override val client: OkHttpClient
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun getLogoColor(): Int = valLogoColor
|
||||
|
||||
override fun getLogo(): Int = valLogo
|
||||
|
||||
override fun getStatusList(): List<Long> = valStatuses
|
||||
|
||||
@@ -18,7 +18,6 @@ class XLogLogcatLogger : LogcatLogger {
|
||||
LogPriority.INFO -> LogLevel.Info.int
|
||||
LogPriority.DEBUG -> LogLevel.Debug.int
|
||||
LogPriority.VERBOSE -> LogLevel.Verbose.int
|
||||
else -> LogLevel.All.int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaDexLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
override fun handleResult(uri: Uri) {
|
||||
val code = uri.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
|
||||
@@ -44,6 +44,8 @@ class ApiMangaParser(
|
||||
coverFileName: String?,
|
||||
coverQuality: String,
|
||||
altTitlesInDesc: Boolean,
|
||||
finalChapterInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga {
|
||||
val mangaId = getManga.await(manga.url, sourceId)?.id
|
||||
val metadata = if (mangaId != null) {
|
||||
@@ -53,7 +55,17 @@ class ApiMangaParser(
|
||||
newMetaInstance()
|
||||
}
|
||||
|
||||
parseIntoMetadata(metadata, input, simpleChapters, statistics, coverFileName, coverQuality, altTitlesInDesc)
|
||||
parseIntoMetadata(
|
||||
metadata,
|
||||
input,
|
||||
simpleChapters,
|
||||
statistics,
|
||||
coverFileName,
|
||||
coverQuality,
|
||||
altTitlesInDesc,
|
||||
finalChapterInDesc,
|
||||
preferExtensionLangTitle,
|
||||
)
|
||||
if (mangaId != null) {
|
||||
metadata.mangaId = mangaId
|
||||
insertFlatMetadata.await(metadata.flatten())
|
||||
@@ -70,13 +82,17 @@ class ApiMangaParser(
|
||||
coverFileName: String?,
|
||||
coverQuality: String,
|
||||
altTitlesInDesc: Boolean,
|
||||
finalChapterInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
) {
|
||||
with(metadata) {
|
||||
try {
|
||||
val mangaAttributesDto = mangaDto.data.attributes
|
||||
mdUuid = mangaDto.data.id
|
||||
title = MdUtil.getTitleFromManga(mangaAttributesDto, lang)
|
||||
altTitles = mangaAttributesDto.altTitles.mapNotNull { it[lang] }.nullIfEmpty()
|
||||
title = MdUtil.getTitleFromManga(mangaAttributesDto, lang, preferExtensionLangTitle)
|
||||
altTitles = mangaAttributesDto.altTitles
|
||||
.filter { it.containsKey(lang) || it.containsKey("${mangaAttributesDto.originalLanguage}-ro") }
|
||||
.mapNotNull { it.values.singleOrNull() }.nullIfEmpty()
|
||||
|
||||
val mangaRelationshipsDto = mangaDto.data.relationships
|
||||
cover = if (!coverFileName.isNullOrEmpty()) {
|
||||
@@ -96,9 +112,19 @@ class ApiMangaParser(
|
||||
originalLanguage = mangaAttributesDto.originalLanguage,
|
||||
).orEmpty()
|
||||
|
||||
val cleanDesc = MdUtil.cleanDescription(rawDesc)
|
||||
|
||||
description = if (altTitlesInDesc) MdUtil.addAltTitleToDesc(cleanDesc, altTitles) else cleanDesc
|
||||
description = MdUtil.cleanDescription(rawDesc)
|
||||
.let { if (altTitlesInDesc) MdUtil.addAltTitleToDesc(it, altTitles) else it }
|
||||
.let {
|
||||
if (finalChapterInDesc) {
|
||||
MdUtil.addFinalChapterToDesc(
|
||||
it,
|
||||
mangaAttributesDto.lastVolume,
|
||||
mangaAttributesDto.lastChapter,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
authors = mangaRelationshipsDto.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MdConstants.Types.author, true)
|
||||
@@ -148,7 +174,11 @@ class ApiMangaParser(
|
||||
mangaAttributesDto.contentRating
|
||||
?.takeUnless { it == "safe" }
|
||||
?.let {
|
||||
RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
|
||||
RaisedTag(
|
||||
"Content Rating",
|
||||
it.capitalize(Locale.US),
|
||||
MangaDexSearchMetadata.TAG_TYPE_DEFAULT,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import exh.md.service.MangaDexService
|
||||
import exh.md.utils.MdConstants
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.md.utils.mdListCall
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -21,7 +20,6 @@ class MangaHandler(
|
||||
private val lang: String,
|
||||
private val service: MangaDexService,
|
||||
private val apiMangaParser: ApiMangaParser,
|
||||
private val followsHandler: FollowsHandler,
|
||||
) {
|
||||
suspend fun getMangaDetails(
|
||||
manga: SManga,
|
||||
@@ -29,6 +27,8 @@ class MangaHandler(
|
||||
coverQuality: String,
|
||||
tryUsingFirstVolumeCover: Boolean,
|
||||
altTitlesInDesc: Boolean,
|
||||
finalChapterInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga {
|
||||
return coroutineScope {
|
||||
val mangaId = MdUtil.getMangaId(manga.url)
|
||||
@@ -55,13 +55,31 @@ class MangaHandler(
|
||||
coverFileName?.await(),
|
||||
coverQuality,
|
||||
altTitlesInDesc,
|
||||
finalChapterInDesc,
|
||||
preferExtensionLangTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long, coverQuality: String, tryUsingFirstVolumeCover: Boolean, altTitlesInDesc: Boolean): Observable<SManga> {
|
||||
fun fetchMangaDetailsObservable(
|
||||
manga: SManga,
|
||||
sourceId: Long,
|
||||
coverQuality: String,
|
||||
tryUsingFirstVolumeCover: Boolean,
|
||||
altTitlesInDesc: Boolean,
|
||||
finalChapterInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): Observable<SManga> {
|
||||
return runAsObservable {
|
||||
getMangaDetails(manga, sourceId, coverQuality, tryUsingFirstVolumeCover, altTitlesInDesc)
|
||||
getMangaDetails(
|
||||
manga,
|
||||
sourceId,
|
||||
coverQuality,
|
||||
tryUsingFirstVolumeCover,
|
||||
altTitlesInDesc,
|
||||
finalChapterInDesc,
|
||||
preferExtensionLangTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +110,10 @@ class MangaHandler(
|
||||
}
|
||||
|
||||
private fun getGroupMap(results: List<ChapterDataDto>): Map<String, String> {
|
||||
return results.map { chapter -> chapter.relationships }
|
||||
.flatten()
|
||||
return results
|
||||
.flatMap { it.relationships }
|
||||
.filter { it.type == MdConstants.Types.scanlator }
|
||||
.map { it.id to it.attributes!!.name!! }
|
||||
.toMap()
|
||||
.associate { it.id to it.attributes!!.name!! }
|
||||
}
|
||||
|
||||
suspend fun fetchRandomMangaId(): String {
|
||||
@@ -105,23 +122,6 @@ class MangaHandler(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTrackingInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> {
|
||||
return withIOContext {
|
||||
/*val metadata = async {
|
||||
val mangaUrl = MdUtil.buildMangaUrl(MdUtil.getMangaId(track.tracking_url))
|
||||
val manga = MangaInfo(mangaUrl, track.title)
|
||||
val response = client.newCall(mangaRequest(manga)).await()
|
||||
val metadata = MangaDexSearchMetadata()
|
||||
apiMangaParser.parseIntoMetadata(metadata, response, emptyList())
|
||||
metadata
|
||||
}*/
|
||||
val remoteTrack = async {
|
||||
followsHandler.fetchTrackingInfo(track.tracking_url)
|
||||
}
|
||||
remoteTrack.await() to null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaFromChapterId(chapterId: String): String? {
|
||||
return withIOContext {
|
||||
apiMangaParser.chapterParseForMangaId(service.viewChapter(chapterId))
|
||||
@@ -134,7 +134,9 @@ class MangaHandler(
|
||||
coverQuality: String,
|
||||
tryUsingFirstVolumeCover: Boolean,
|
||||
altTitlesInDesc: Boolean,
|
||||
): SManga? {
|
||||
finalChapterInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga {
|
||||
return withIOContext {
|
||||
val mangaId = MdUtil.getMangaId(track.tracking_url)
|
||||
val response = service.viewManga(mangaId)
|
||||
@@ -154,6 +156,8 @@ class MangaHandler(
|
||||
coverFileName,
|
||||
coverQuality,
|
||||
altTitlesInDesc,
|
||||
finalChapterInDesc,
|
||||
preferExtensionLangTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,15 @@ package exh.md.utils
|
||||
import android.app.Application
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
import exh.md.dto.MangaAttributesDto
|
||||
import exh.md.dto.MangaDataDto
|
||||
import exh.source.getMainSource
|
||||
import exh.util.dropBlank
|
||||
import exh.util.floor
|
||||
import exh.util.nullIfZero
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
@@ -25,7 +21,9 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.parser.Parser
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -39,21 +37,10 @@ class MdUtil {
|
||||
const val baseUrl = "https://mangadex.org"
|
||||
const val chapterSuffix = "/chapter/"
|
||||
|
||||
const val similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
|
||||
const val similarCacheMangas = "https://api.similarmanga.com/manga/"
|
||||
const val similarBaseApi = "https://api.similarmanga.com/similar/"
|
||||
|
||||
const val groupSearchUrl = "$baseUrl/groups/0/1/"
|
||||
const val reportUrl = "https://api.mangadex.network/report"
|
||||
|
||||
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
|
||||
const val mangaLimit = 20
|
||||
|
||||
/**
|
||||
* Get the manga offset pages are 1 based, so subtract 1
|
||||
*/
|
||||
fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString()
|
||||
|
||||
val jsonParser =
|
||||
Json {
|
||||
isLenient = true
|
||||
@@ -65,15 +52,8 @@ class MdUtil {
|
||||
|
||||
private const val scanlatorSeparator = " & "
|
||||
|
||||
const val contentRatingSafe = "safe"
|
||||
const val contentRatingSuggestive = "suggestive"
|
||||
const val contentRatingErotica = "erotica"
|
||||
const val contentRatingPornographic = "pornographic"
|
||||
|
||||
val validOneShotFinalChapters = listOf("0", "1")
|
||||
|
||||
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
|
||||
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
|
||||
val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
|
||||
val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
|
||||
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
|
||||
|
||||
fun buildMangaUrl(mangaUuid: String): String {
|
||||
@@ -94,47 +74,10 @@ class MdUtil {
|
||||
.trim()
|
||||
}
|
||||
|
||||
fun getImageUrl(attr: String): String {
|
||||
// Some images are hosted elsewhere
|
||||
if (attr.startsWith("http")) {
|
||||
return attr
|
||||
}
|
||||
return baseUrl + attr
|
||||
}
|
||||
|
||||
fun getScanlators(scanlators: String?): Set<String> {
|
||||
return scanlators?.split(scanlatorSeparator)?.dropBlank()?.toSet().orEmpty()
|
||||
}
|
||||
|
||||
fun getScanlatorString(scanlators: Set<String>): String {
|
||||
return scanlators.sorted().joinToString(scanlatorSeparator)
|
||||
}
|
||||
|
||||
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
|
||||
if (mangaStatus == SManga.COMPLETED) return null
|
||||
|
||||
val remove0ChaptersFromCount = chapters.distinctBy {
|
||||
/*if (it.chapter_txt.isNotEmpty()) {
|
||||
it.vol + it.chapter_txt
|
||||
} else {*/
|
||||
it.name
|
||||
/*}*/
|
||||
}.sortedByDescending { it.chapter_number }
|
||||
|
||||
remove0ChaptersFromCount.firstOrNull()?.let { chapter ->
|
||||
val chpNumber = chapter.chapter_number.floor()
|
||||
val allChapters = (1..chpNumber).toMutableSet()
|
||||
|
||||
remove0ChaptersFromCount.forEach {
|
||||
allChapters.remove(it.chapter_number.floor())
|
||||
}
|
||||
|
||||
if (allChapters.isEmpty()) return null
|
||||
return allChapters.size.toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
@@ -144,7 +87,7 @@ class MdUtil {
|
||||
fun createMangaEntry(json: MangaDataDto, lang: String): SManga {
|
||||
return SManga(
|
||||
url = buildMangaUrl(json.id),
|
||||
title = getTitleFromManga(json.attributes, lang),
|
||||
title = getTitleFromManga(json.attributes, lang, true),
|
||||
thumbnail_url = json.relationships
|
||||
.firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
|
||||
?.attributes
|
||||
@@ -155,12 +98,30 @@ class MdUtil {
|
||||
)
|
||||
}
|
||||
|
||||
fun getTitleFromManga(json: MangaAttributesDto, lang: String): String {
|
||||
return getFromLangMap(json.title.asMdMap(), lang, json.originalLanguage)
|
||||
?: getAltTitle(json.altTitles, lang, json.originalLanguage)
|
||||
?: json.title.asMdMap<String>()[json.originalLanguage]
|
||||
?: json.altTitles.firstNotNullOfOrNull { it[json.originalLanguage] }
|
||||
.orEmpty()
|
||||
fun getTitleFromManga(json: MangaAttributesDto, lang: String, preferExtensionLangTitle: Boolean): String {
|
||||
val titleMap = json.title.asMdMap<String>()
|
||||
val altTitles = json.altTitles
|
||||
val originalLang = json.originalLanguage
|
||||
|
||||
titleMap[lang]?.let { return it }
|
||||
|
||||
val mainTitle = titleMap.values.firstOrNull()
|
||||
val langTitle = findTitleInMaps(lang, titleMap, altTitles)
|
||||
val enTitle = findTitleInMaps("en", titleMap, altTitles)
|
||||
val originalLangTitle = findTitleInMaps("$originalLang-ro", titleMap, altTitles) ?: findTitleInMaps(
|
||||
originalLang,
|
||||
titleMap,
|
||||
altTitles,
|
||||
)
|
||||
|
||||
val ordered = if (preferExtensionLangTitle) {
|
||||
listOf(langTitle, mainTitle, enTitle, originalLangTitle)
|
||||
} else {
|
||||
listOf(mainTitle, langTitle, enTitle, originalLangTitle)
|
||||
}
|
||||
|
||||
return ordered.firstOrNull { it != null }
|
||||
?: ""
|
||||
}
|
||||
|
||||
fun getFromLangMap(langMap: Map<String, String>, currentLang: String, originalLanguage: String): String? {
|
||||
@@ -174,15 +135,12 @@ class MdUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getAltTitle(langMaps: List<Map<String, String>>, currentLang: String, originalLanguage: String): String? {
|
||||
return langMaps.firstNotNullOfOrNull { it[currentLang] }
|
||||
?: langMaps.firstNotNullOfOrNull { it["en"] }
|
||||
?: if (originalLanguage == "ja") {
|
||||
langMaps.firstNotNullOfOrNull { it["ja-ro"] }
|
||||
?: langMaps.firstNotNullOfOrNull { it["jp-ro"] }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
fun findTitleInMaps(
|
||||
lang: String,
|
||||
titleMap: Map<String, String>,
|
||||
altTitleMaps: List<Map<String, String>>,
|
||||
): String? {
|
||||
return titleMap[lang] ?: altTitleMaps.firstNotNullOfOrNull { it[lang] }
|
||||
}
|
||||
|
||||
fun cdnCoverUrl(dexId: String, fileName: String): String {
|
||||
@@ -200,7 +158,7 @@ class MdUtil {
|
||||
fun loadOAuth(preferences: TrackPreferences, mdList: MdList): MALOAuth? {
|
||||
return try {
|
||||
jsonParser.decodeFromString<MALOAuth>(preferences.trackToken(mdList).get())
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -230,7 +188,10 @@ class MdUtil {
|
||||
return codeVerifier ?: PkceUtil.generateCodeVerifier().also { codeVerifier = it }
|
||||
}
|
||||
|
||||
fun getEnabledMangaDex(sourcePreferences: SourcePreferences = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
|
||||
fun getEnabledMangaDex(
|
||||
sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
): MangaDex? {
|
||||
return getEnabledMangaDexs(sourcePreferences, sourceManager).let { mangadexs ->
|
||||
sourcePreferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()
|
||||
?.let { preferredMangaDexId ->
|
||||
@@ -240,7 +201,10 @@ class MdUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getEnabledMangaDexs(preferences: SourcePreferences, sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
|
||||
fun getEnabledMangaDexs(
|
||||
preferences: SourcePreferences,
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
): List<MangaDex> {
|
||||
val languages = preferences.enabledLanguages().get()
|
||||
val disabledSourceIds = preferences.disabledSources().get()
|
||||
|
||||
@@ -262,8 +226,30 @@ class MdUtil {
|
||||
description
|
||||
} else {
|
||||
val altTitlesDesc = altTitles
|
||||
.joinToString("\n", "${Injekt.get<Application>().getString(R.string.alt_titles)}:\n") { "• $it" }
|
||||
description + (if (description.isBlank()) "" else "\n\n") + Parser.unescapeEntities(altTitlesDesc, false)
|
||||
.joinToString(
|
||||
"\n",
|
||||
"${Injekt.get<Application>().stringResource(SYMR.strings.alt_titles)}:\n",
|
||||
) { "• $it" }
|
||||
description + (if (description.isBlank()) "" else "\n\n") + Parser.unescapeEntities(
|
||||
altTitlesDesc,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addFinalChapterToDesc(description: String, lastVolume: String?, lastChapter: String?): String {
|
||||
val parts = listOfNotNull(
|
||||
lastVolume?.takeIf { it.isNotEmpty() }?.let { "Vol.$it" },
|
||||
lastChapter?.takeIf { it.isNotEmpty() }?.let { "Ch.$it" },
|
||||
)
|
||||
|
||||
return if (parts.isEmpty()) {
|
||||
description
|
||||
} else {
|
||||
description + (if (description.isBlank()) "" else "\n\n") + parts.joinToString(
|
||||
" ",
|
||||
"${Injekt.get<Application>().stringResource(SYMR.strings.final_chapter)}:\n",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package exh.ui.login
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.CookieManager
|
||||
@@ -13,6 +12,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.presentation.webview.EhLoginWebViewScreen
|
||||
import eu.kanade.presentation.webview.components.IgneousDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -92,16 +92,32 @@ class EhLoginActivity : BaseActivity() {
|
||||
|
||||
private fun onPageFinished(view: WebView, url: String, customIgneous: String?) {
|
||||
xLogD(url)
|
||||
val parsedUrl = Uri.parse(url)
|
||||
val parsedUrl = url.toUri()
|
||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
}
|
||||
// Check login result
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
let html = document.documentElement.innerHTML;
|
||||
return html.includes("/cdn-cgi/");
|
||||
})();
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
val isCloudflareBlock = result == "true"
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
if (isCloudflareBlock) {
|
||||
xLogD("Cloudflare block detected — skipping logic")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
}
|
||||
// Check login result
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
}
|
||||
}
|
||||
} else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
|
||||
// At ExHentai, check that everything worked out...
|
||||
@@ -155,9 +171,9 @@ class EhLoginActivity : BaseActivity() {
|
||||
if (memberId == null || passHash == null || igneous == null) return false
|
||||
|
||||
// Update prefs
|
||||
exhPreferences.memberIdVal().set(memberId!!)
|
||||
exhPreferences.passHashVal().set(passHash!!)
|
||||
exhPreferences.igneousVal().set(igneous!!)
|
||||
exhPreferences.memberIdVal().set(memberId)
|
||||
exhPreferences.passHashVal().set(passHash)
|
||||
exhPreferences.igneousVal().set(igneous)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
import exh.source.PURURIN_SOURCE_ID
|
||||
import exh.source.TSUMINO_SOURCE_ID
|
||||
import exh.source.lanraragiSourceIds
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.source.nHentaiSourceIds
|
||||
import java.util.Locale
|
||||
@@ -23,7 +24,8 @@ object SourceTagsUtil {
|
||||
sourceId in nHentaiSourceIds ||
|
||||
sourceId in mangaDexSourceIds ||
|
||||
sourceId == PURURIN_SOURCE_ID ||
|
||||
sourceId == TSUMINO_SOURCE_ID
|
||||
sourceId == TSUMINO_SOURCE_ID ||
|
||||
sourceId in lanraragiSourceIds
|
||||
) {
|
||||
val parsed = when {
|
||||
fullTag != null -> parseTag(fullTag)
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -62,7 +63,6 @@ import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.Pill
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -144,7 +144,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
||||
onClick = {
|
||||
@@ -331,13 +331,13 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
|
||||
private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) {
|
||||
mutableState.update { state ->
|
||||
val updatedSources = action(state.sources)
|
||||
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
||||
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
if (save) saveSources()
|
||||
saveSources()
|
||||
}
|
||||
|
||||
private fun initSources() {
|
||||
@@ -370,7 +370,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
}
|
||||
.toList()
|
||||
|
||||
updateSources(save = false) { sources }
|
||||
mutableState.update { state ->
|
||||
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelection(id: Long) {
|
||||
|
||||
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
|
||||
}
|
||||
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
State(
|
||||
current = current,
|
||||
target = target,
|
||||
applicableFlags = applicableFlags,
|
||||
|
||||
@@ -54,9 +54,11 @@ fun CalenderHeader(
|
||||
}
|
||||
Row {
|
||||
IconButton(onClick = onPreviousClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
||||
}
|
||||
IconButton(onClick = onNextClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user