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