Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dea38912fc | |||
| 5243346356 | |||
| 6bb3ec5b8a | |||
| 7390e72045 | |||
| 64ff5cb9af | |||
| 82f011e48e | |||
| 8868a5db2b | |||
| a335feedfc | |||
| 90ff5e69ec | |||
| 68a1820695 | |||
| 292b551027 | |||
| e19c62a8ae | |||
| 348cb335c4 | |||
| c426d11d76 | |||
| 1965c0825d | |||
| 0124763fcd | |||
| b0d737592c | |||
| e14596465b | |||
| 5e52dfcc66 | |||
| a09643fc77 | |||
| 19bc08659b | |||
| 2cb8f8f872 | |||
| 23c7bb09d3 | |||
| bdb8553e28 | |||
| fbac29e0cd | |||
| b0aa2ffc42 | |||
| 45b5d9b8a4 | |||
| 91b98cdb82 | |||
| 7f544f7163 | |||
| 3705880a77 | |||
| 759fd4d4e3 | |||
| fc956fc791 | |||
| 9d7346157b | |||
| 0f0f4cf4a9 | |||
| 426ef65102 | |||
| 95c834581b | |||
| 71f2daf8f3 |
@@ -36,7 +36,7 @@ jobs:
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew detekt assembleDevDebug
|
||||
run: ./gradlew spotlessCheck assembleDevDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# SY -->
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew detekt assembleStandardRelease testStandardReleaseUnitTest --stacktrace
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testStandardReleaseUnitTest --stacktrace
|
||||
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Label PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
label_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check PR and Add Label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
if (prAuthor === 'weblate') {
|
||||
const labels = ['Translations'];
|
||||
await github.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
+7
-14
@@ -1,4 +1,4 @@
|
||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
|
||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](/README.md#issues-feature-requests-and-contributing).
|
||||
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ Thanks for your interest in contributing to Tachiyomi!
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||
If you're interested in taking on [an open issue](https://github.com/jobobby04/TachiyomiSY/issues), please comment on it so others are aware.
|
||||
You do not need to ask for permission nor an assignment.
|
||||
|
||||
## Prerequisites
|
||||
@@ -24,34 +24,27 @@ Before you start, please note that the ability to use following technologies is
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- Emulator or phone with developer options enabled to test changes.
|
||||
|
||||
## Linting
|
||||
|
||||
Run the `detekt` gradle task. If the build fails, a report of issues can be found in `app/build/reports/detekt/`. The report is availble in several formats and details each issue that needs attention.
|
||||
|
||||
## Getting help
|
||||
|
||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||
- Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
|
||||
|
||||
# Translations
|
||||
|
||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.
|
||||
Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details.
|
||||
|
||||
|
||||
# Forks
|
||||
|
||||
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
|
||||
Forks are allowed so long as they abide by [the project's LICENSE](/LICENSE).
|
||||
|
||||
When creating a fork, remember to:
|
||||
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- Change the app icon
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
||||
- Change or disable the [app update checker](/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
||||
- To avoid installation conflicts:
|
||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
||||
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
||||
- Change the `applicationId` in [`build.gradle.kts`](/app/build.gradle.kts)
|
||||
|
||||
|
||||
### Supporting Cloud Sync - Google Drive Implementation
|
||||
|
||||
@@ -82,7 +82,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<details><summary>Issues</summary>
|
||||
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/mihon)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -21,7 +21,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
|
||||
// shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
@@ -38,7 +38,7 @@ android {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += SUPPORTED_ABIS
|
||||
abiFilters += supportedAbis
|
||||
}
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ android {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*SUPPORTED_ABIS.toTypedArray())
|
||||
include(*supportedAbis.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ dependencies {
|
||||
implementation(compose.ui.tooling.preview)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
|
||||
|
||||
implementation(androidx.interpolator)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
@@ -212,10 +212,6 @@ dependencies {
|
||||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.bundles.archive)
|
||||
// SY -->
|
||||
implementation(libs.zip4j)
|
||||
// SY <--
|
||||
|
||||
// Preferences
|
||||
implementation(libs.preferencektx)
|
||||
@@ -247,7 +243,6 @@ dependencies {
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
|
||||
Vendored
-3
@@ -127,9 +127,6 @@
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||
|
||||
# Apache Commons Compress
|
||||
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
|
||||
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||
</adaptive-icon>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||
</adaptive-icon>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -24,6 +24,7 @@ import eu.kanade.domain.track.interactor.RefreshTracks
|
||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
@@ -152,6 +153,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
|
||||
@@ -32,10 +32,11 @@ class GetEnabledSources(
|
||||
) { a, b, c -> Triple(a, b, c) },
|
||||
// SY <--
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds,
|
||||
(enabledLanguages, disabledSources, lastUsedSource),
|
||||
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
|
||||
sources,
|
||||
) {
|
||||
pinnedSourceIds,
|
||||
(enabledLanguages, disabledSources, lastUsedSource),
|
||||
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
|
||||
sources,
|
||||
->
|
||||
|
||||
val sourcesAndCategories = sourcesInCategories.map {
|
||||
|
||||
@@ -13,7 +13,7 @@ class SyncPreferences(
|
||||
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
||||
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
||||
|
||||
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||
|
||||
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
|
||||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||
|
||||
@@ -18,12 +18,20 @@ class UiPreferences(
|
||||
|
||||
fun themeMode() = preferenceStore.getEnum(
|
||||
"pref_theme_mode_key",
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ThemeMode.SYSTEM
|
||||
} else {
|
||||
ThemeMode.LIGHT
|
||||
},
|
||||
)
|
||||
|
||||
fun appTheme() = preferenceStore.getEnum(
|
||||
"pref_app_theme",
|
||||
if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT },
|
||||
if (DeviceUtil.isDynamicColorAvailable) {
|
||||
AppTheme.MONET
|
||||
} else {
|
||||
AppTheme.DEFAULT
|
||||
},
|
||||
)
|
||||
|
||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
|
||||
@@ -240,7 +240,7 @@ private fun DetailsHeader(
|
||||
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
||||
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
||||
NSFW: ${extension.isNsfw}
|
||||
""".trimIndent()
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (extension is Extension.Installed) {
|
||||
@@ -250,8 +250,8 @@ private fun DetailsHeader(
|
||||
Update available: ${extension.hasUpdate}
|
||||
Obsolete: ${extension.isObsolete}
|
||||
Shared: ${extension.isShared}
|
||||
Repository: ${extension.repoUrl}
|
||||
""".trimIndent()
|
||||
Repository: ${extension.repoUrl}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,9 @@ private fun ExtensionContent(
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
@@ -241,7 +243,9 @@ private fun ExtensionContent(
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -179,7 +179,7 @@ private fun SourcePinButton(
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = SecondaryItemAlpha,
|
||||
alpha = SECONDARY_ALPHA,
|
||||
)
|
||||
}
|
||||
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin
|
||||
|
||||
@@ -79,7 +79,7 @@ fun TabbedDialog(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
pageContent = { page -> content(page) }
|
||||
pageContent = { page -> content(page) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,6 @@ private fun ColumnScope.SortPage(
|
||||
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
|
||||
// SY <--
|
||||
|
||||
|
||||
val trackerSortOption = if (trackers.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
|
||||
@@ -62,7 +62,7 @@ private val ContinueReadingButtonIconSizeLarge = 20.dp
|
||||
private val ContinueReadingButtonGridPadding = 6.dp
|
||||
private val ContinueReadingButtonListSpacing = 8.dp
|
||||
|
||||
private const val GridSelectedCoverAlpha = 0.76f
|
||||
private const val GRID_SELECTED_COVER_ALPHA = 0.76f
|
||||
|
||||
/**
|
||||
* Layout of grid list item with title overlaying the cover.
|
||||
@@ -90,7 +90,7 @@ fun MangaCompactGridItem(
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
|
||||
.alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
|
||||
data = coverData,
|
||||
)
|
||||
},
|
||||
@@ -197,7 +197,7 @@ fun MangaComfortableGridItem(
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
|
||||
.alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
|
||||
data = coverData,
|
||||
)
|
||||
},
|
||||
@@ -371,7 +371,7 @@ fun MangaListItem(
|
||||
size = ContinueReadingButtonSizeSmall,
|
||||
iconSize = ContinueReadingButtonIconSizeSmall,
|
||||
onClick = onClickContinueReading,
|
||||
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing)
|
||||
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,7 @@ private fun ContinueReadingButton(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
|
||||
),
|
||||
modifier = Modifier.size(size)
|
||||
modifier = Modifier.size(size),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
|
||||
@@ -77,6 +77,7 @@ import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
|
||||
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil
|
||||
@@ -578,6 +579,7 @@ private fun MangaScreenSmallImpl(
|
||||
|
||||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
mergedData = state.mergedData,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
@@ -880,6 +882,7 @@ fun MangaScreenLargeImpl(
|
||||
|
||||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
mergedData = state.mergedData,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
@@ -944,6 +947,7 @@ private fun SharedMangaBottomActionMenu(
|
||||
|
||||
private fun LazyListScope.sharedChapterItems(
|
||||
manga: Manga,
|
||||
mergedData: MergedMangaData?,
|
||||
chapters: List<ChapterList>,
|
||||
isAnyChapterSelected: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
@@ -995,7 +999,9 @@ private fun LazyListScope.sharedChapterItems(
|
||||
// SY <--
|
||||
},
|
||||
readProgress = item.chapter.lastPageRead
|
||||
.takeIf { /* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L }
|
||||
.takeIf {
|
||||
/* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L
|
||||
}
|
||||
?.let {
|
||||
stringResource(
|
||||
MR.strings.chapter_progress,
|
||||
@@ -1011,7 +1017,8 @@ private fun LazyListScope.sharedChapterItems(
|
||||
read = item.chapter.read,
|
||||
bookmark = item.chapter.bookmark,
|
||||
selected = item.selected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
|
||||
downloadIndicatorEnabled =
|
||||
!isAnyChapterSelected && !(mergedData?.manga?.get(item.chapter.mangaId) ?: manga).isLocal(),
|
||||
downloadStateProvider = { item.downloadState },
|
||||
downloadProgressProvider = { item.downloadProgress },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -60,6 +60,6 @@ private fun MissingChaptersWarning(count: Int) {
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error.copy(alpha = SecondaryItemAlpha),
|
||||
color = MaterialTheme.colorScheme.error.copy(alpha = SECONDARY_ALPHA),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.selectedBackground
|
||||
|
||||
@@ -135,7 +135,7 @@ fun MangaChapterListItem(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f),
|
||||
color = LocalContentColor.current.copy(alpha = if (read) DISABLED_ALPHA else 1f),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ fun MangaChapterListItem(
|
||||
val subtitleStyle = MaterialTheme.typography.bodySmall
|
||||
.merge(
|
||||
color = LocalContentColor.current
|
||||
.copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha)
|
||||
.copy(alpha = if (read) DISABLED_ALPHA else SECONDARY_ALPHA),
|
||||
)
|
||||
ProvideTextStyle(value = subtitleStyle) {
|
||||
if (date != null) {
|
||||
@@ -152,14 +152,19 @@ fun MangaChapterListItem(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
if (readProgress != null ||
|
||||
scanlator != null/* SY --> */ ||
|
||||
sourceName != null/* SY <-- */
|
||||
) {
|
||||
DotSeparatorText()
|
||||
}
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
|
||||
)
|
||||
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
@@ -181,7 +182,7 @@ fun MangaActionRow(
|
||||
// SY <--
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA)
|
||||
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
|
||||
@@ -44,7 +44,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
private fun PagePreviewLoading(
|
||||
setMaxWidth: (Dp) -> Unit
|
||||
setMaxWidth: (Dp) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
@@ -63,7 +63,7 @@ private fun PagePreviewLoading(
|
||||
@Composable
|
||||
private fun PagePreviewRow(
|
||||
onOpenPage: (Int) -> Unit,
|
||||
items: ImmutableList<PagePreview>
|
||||
items: ImmutableList<PagePreview>,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -88,7 +88,7 @@ private fun PagePreviewMore(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TextButton(onClick = onMorePreviewsClicked) {
|
||||
Text(stringResource(SYMR.strings.more_previews))
|
||||
@@ -116,7 +116,7 @@ fun PagePreviews(
|
||||
pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
items = remember(it) { it.toImmutableList() }
|
||||
items = remember(it) { it.toImmutableList() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ fun LazyListScope.PagePreviewItems(
|
||||
) {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
items = remember(it) { it.toImmutableList() }
|
||||
items = remember(it) { it.toImmutableList() },
|
||||
)
|
||||
}
|
||||
item(
|
||||
|
||||
@@ -75,7 +75,7 @@ private fun NewUpdateScreenPreview() {
|
||||
changelogInfo = """
|
||||
## Yay
|
||||
Foobar
|
||||
|
||||
|
||||
### More info
|
||||
- Hello
|
||||
- World
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@ package eu.kanade.presentation.more.settings.screen
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
@@ -376,7 +375,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
chooseColorProfile.launch(arrayOf("*/*"))
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -180,11 +180,15 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = previewsRowCount,
|
||||
title = stringResource(SYMR.strings.pref_previews_row_count),
|
||||
subtitle = if (previewsRowCount > 0) pluralStringResource(
|
||||
SYMR.plurals.row_count,
|
||||
previewsRowCount,
|
||||
previewsRowCount,
|
||||
) else stringResource(MR.strings.disabled),
|
||||
subtitle = if (previewsRowCount > 0) {
|
||||
pluralStringResource(
|
||||
SYMR.plurals.row_count,
|
||||
previewsRowCount,
|
||||
previewsRowCount,
|
||||
)
|
||||
} else {
|
||||
stringResource(MR.strings.disabled)
|
||||
},
|
||||
min = 0,
|
||||
max = 10,
|
||||
onValueChanged = {
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
pref = uiPreferences.feedTabInFront(),
|
||||
title = stringResource(SYMR.strings.pref_feed_position),
|
||||
subtitle = stringResource(SYMR.strings.pref_feed_position_summery),
|
||||
enabled = hideFeedTab.not()
|
||||
enabled = hideFeedTab.not(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+19
-3
@@ -394,11 +394,23 @@ object SettingsDataScreen : SearchableSettings {
|
||||
syncServiceType: SyncManager.SyncService,
|
||||
syncPreferences: SyncPreferences,
|
||||
): List<Preference> {
|
||||
return when (syncServiceType) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val preferences = when (syncServiceType) {
|
||||
SyncManager.SyncService.NONE -> emptyList()
|
||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||
}
|
||||
|
||||
return if (syncServiceType != SyncManager.SyncService.NONE) {
|
||||
preferences + Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(SYMR.strings.pref_choose_what_to_sync),
|
||||
onClick = {
|
||||
navigator.push(SyncSettingsSelector())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
preferences
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -515,7 +527,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(SYMR.strings.pref_sync_now_group_title),
|
||||
preferenceItems = persistentListOf(
|
||||
@@ -524,7 +536,11 @@ object SettingsDataScreen : SearchableSettings {
|
||||
title = stringResource(SYMR.strings.pref_sync_now),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
||||
onClick = {
|
||||
navigator.push(SyncSettingsSelector())
|
||||
if (!SyncDataJob.isRunning(context)) {
|
||||
SyncDataJob.startNow(context)
|
||||
} else {
|
||||
context.toast(SYMR.strings.sync_in_progress)
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+6
@@ -120,6 +120,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
allCategories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters()
|
||||
val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly()
|
||||
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories()
|
||||
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude()
|
||||
|
||||
@@ -152,6 +153,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
pref = downloadNewChaptersPref,
|
||||
title = stringResource(MR.strings.pref_download_new),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewUnreadChaptersOnlyPref,
|
||||
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
|
||||
enabled = downloadNewChapters,
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.categories),
|
||||
subtitle = getCategoriesLabel(
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
name = it.name,
|
||||
website = it.website,
|
||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
)
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
+9
-2
@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConfirmDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
@@ -32,7 +33,7 @@ class ExtensionReposScreen(
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(url) {
|
||||
url?.let { screenModel.createRepo(it) }
|
||||
url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) }
|
||||
}
|
||||
|
||||
if (state is RepoScreenState.Loading) {
|
||||
@@ -67,7 +68,6 @@ class ExtensionReposScreen(
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
|
||||
is RepoDialog.Conflict -> {
|
||||
ExtensionRepoConflictDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
@@ -76,6 +76,13 @@ class ExtensionReposScreen(
|
||||
newRepo = dialog.newRepo,
|
||||
)
|
||||
}
|
||||
is RepoDialog.Confirm -> {
|
||||
ExtensionRepoConfirmDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(dialog.url) },
|
||||
repo = dialog.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+1
@@ -125,6 +125,7 @@ sealed class RepoDialog {
|
||||
data object Create : RepoDialog()
|
||||
data class Delete(val repo: String) : RepoDialog()
|
||||
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
||||
data class Confirm(val url: String) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class RepoScreenState {
|
||||
|
||||
+32
@@ -152,3 +152,35 @@ fun ExtensionRepoConflictDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoConfirmDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: () -> Unit,
|
||||
repo: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_add_repo))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.add_repo_confirmation, repo))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onCreate()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
|
||||
+3
-11
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@@ -16,7 +15,6 @@ import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -32,7 +30,6 @@ import uy.kohesive.injekt.api.get
|
||||
class SyncSettingsSelector : Screen() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { SyncSettingsSelectorModel() }
|
||||
val state by model.state.collectAsState()
|
||||
@@ -48,15 +45,10 @@ class SyncSettingsSelector : Screen() {
|
||||
) { contentPadding ->
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(SYMR.strings.label_sync),
|
||||
actionEnabled = state.options.canCreate(),
|
||||
actionLabel = stringResource(MR.strings.action_save),
|
||||
actionEnabled = true,
|
||||
onClickAction = {
|
||||
if (!SyncDataJob.isRunning(context)) {
|
||||
model.syncNow(context)
|
||||
navigator.pop()
|
||||
} else {
|
||||
context.toast(SYMR.strings.sync_in_progress)
|
||||
}
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
item {
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
class BackupSchemaScreen : Screen() {
|
||||
|
||||
companion object {
|
||||
const val title = "Backup file schema"
|
||||
const val TITLE = "Backup file schema"
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -41,7 +41,7 @@ class BackupSchemaScreen : Screen() {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = title,
|
||||
title = TITLE,
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
@@ -50,7 +50,7 @@ class BackupSchemaScreen : Screen() {
|
||||
title = stringResource(MR.strings.action_copy_to_clipboard),
|
||||
icon = Icons.Default.ContentCopy,
|
||||
onClick = {
|
||||
context.copyToClipboard(title, schema)
|
||||
context.copyToClipboard(TITLE, schema)
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ class DebugInfoScreen : Screen() {
|
||||
itemsProvider = {
|
||||
listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = WorkerInfoScreen.title,
|
||||
title = WorkerInfoScreen.TITLE,
|
||||
onClick = { navigator.push(WorkerInfoScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = BackupSchemaScreen.title,
|
||||
title = BackupSchemaScreen.TITLE,
|
||||
onClick = { navigator.push(BackupSchemaScreen()) },
|
||||
),
|
||||
getAppInfoGroup(),
|
||||
|
||||
+4
-4
@@ -49,7 +49,7 @@ import java.time.ZoneId
|
||||
class WorkerInfoScreen : Screen() {
|
||||
|
||||
companion object {
|
||||
const val title = "Worker info"
|
||||
const val TITLE = "Worker info"
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -65,7 +65,7 @@ class WorkerInfoScreen : Screen() {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = title,
|
||||
title = TITLE,
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
@@ -74,7 +74,7 @@ class WorkerInfoScreen : Screen() {
|
||||
title = stringResource(MR.strings.action_copy_to_clipboard),
|
||||
icon = Icons.Default.ContentCopy,
|
||||
onClick = {
|
||||
context.copyToClipboard(title, enqueued + finished + running)
|
||||
context.copyToClipboard(TITLE, enqueued + finished + running)
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -159,7 +159,7 @@ class WorkerInfoScreen : Screen() {
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
),
|
||||
)
|
||||
appendLine("Next scheduled run: $timestamp",)
|
||||
appendLine("Next scheduled run: $timestamp")
|
||||
appendLine("Attempt #${workInfo.runAttemptCount + 1}")
|
||||
}
|
||||
appendLine()
|
||||
|
||||
@@ -34,7 +34,9 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
||||
|
||||
private enum class State {
|
||||
CHECKED, INVERSED, UNCHECKED
|
||||
CHECKED,
|
||||
INVERSED,
|
||||
UNCHECKED,
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
@@ -73,7 +73,7 @@ private fun RowScope.BaseStatsItem(
|
||||
style = subtitleStyle
|
||||
.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
.copy(alpha = SecondaryItemAlpha),
|
||||
.copy(alpha = SECONDARY_ALPHA),
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -226,7 +226,7 @@ private fun ChapterText(
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
if (downloaded) {
|
||||
appendInlineContent(DownloadedIconContentId)
|
||||
appendInlineContent(DOWNLOADED_ICON_ID)
|
||||
append(' ')
|
||||
}
|
||||
append(name)
|
||||
@@ -236,7 +236,7 @@ private fun ChapterText(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
inlineContent = persistentMapOf(
|
||||
DownloadedIconContentId to InlineTextContent(
|
||||
DOWNLOADED_ICON_ID to InlineTextContent(
|
||||
Placeholder(
|
||||
width = 22.sp,
|
||||
height = 22.sp,
|
||||
@@ -273,7 +273,7 @@ private val CardColor: CardColors
|
||||
)
|
||||
|
||||
private val VerticalSpacerSize = 24.dp
|
||||
private const val DownloadedIconContentId = "downloaded"
|
||||
private const val DOWNLOADED_ICON_ID = "downloaded"
|
||||
|
||||
private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy(
|
||||
id = 0L,
|
||||
|
||||
@@ -63,7 +63,7 @@ fun ExhUtils(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AnimatedVisibility(visible = isVisible) {
|
||||
Column {
|
||||
@@ -84,7 +84,7 @@ fun ExhUtils(
|
||||
) {
|
||||
Column(
|
||||
Modifier.weight(3f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(SYMR.strings.eh_autoscroll),
|
||||
@@ -93,17 +93,17 @@ fun ExhUtils(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.fillMaxWidth(0.75f),
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Switch(
|
||||
checked = isAutoScroll,
|
||||
onCheckedChange = null,
|
||||
enabled = isAutoScrollEnabled
|
||||
enabled = isAutoScrollEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ fun ExhUtils(
|
||||
) {
|
||||
Column(
|
||||
Modifier.weight(3f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
var autoScrollFrequencyState by remember {
|
||||
mutableStateOf(autoScrollFrequency)
|
||||
|
||||
@@ -38,7 +38,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -58,8 +57,6 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private const val UnsetStatusTextAlpha = 0.5F
|
||||
|
||||
@Composable
|
||||
fun TrackInfoDialogHome(
|
||||
trackItems: List<TrackItem>,
|
||||
@@ -211,10 +208,9 @@ private fun TrackInfoItem(
|
||||
if (onScoreClick != null) {
|
||||
VerticalDivider()
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (score == null) UnsetStatusTextAlpha else 1f),
|
||||
text = score ?: stringResource(MR.strings.score),
|
||||
modifier = Modifier.weight(1f),
|
||||
text = score,
|
||||
placeholder = stringResource(MR.strings.score),
|
||||
onClick = onScoreClick,
|
||||
)
|
||||
}
|
||||
@@ -243,6 +239,8 @@ private fun TrackInfoItem(
|
||||
}
|
||||
}
|
||||
|
||||
private const val UNSET_TEXT_ALPHA = 0.5F
|
||||
|
||||
@Composable
|
||||
private fun TrackDetailsItem(
|
||||
text: String?,
|
||||
@@ -263,7 +261,7 @@ private fun TrackDetailsItem(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UNSET_TEXT_ALPHA else 1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ private fun SearchResultItem(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val description = trackSearch.summary.trim()
|
||||
@@ -243,7 +244,10 @@ private fun SearchResultItem(
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = { dropDownMenuExpanded = true },
|
||||
onClick = onClick,
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
|
||||
@@ -43,7 +43,7 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.selectedBackground
|
||||
@@ -107,8 +107,10 @@ internal fun LazyListScope.updatesUiItems(
|
||||
readProgress = updatesItem.update.lastPageRead
|
||||
.takeIf {
|
||||
/* SY --> */(
|
||||
!updatesItem.update.read || (preserveReadingPosition && updatesItem.isEhBasedUpdate())
|
||||
)/* SY <-- */ && it > 0L
|
||||
!updatesItem.update.read ||
|
||||
(preserveReadingPosition && updatesItem.isEhBasedUpdate())
|
||||
)/* SY <-- */ &&
|
||||
it > 0L
|
||||
}
|
||||
?.let {
|
||||
stringResource(
|
||||
@@ -152,7 +154,7 @@ private fun UpdatesUiItem(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val textAlpha = if (update.read) ReadItemAlpha else 1f
|
||||
val textAlpha = if (update.read) DISABLED_ALPHA else 1f
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
@@ -226,7 +228,7 @@ private fun UpdatesUiItem(
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
@@ -28,7 +27,6 @@ import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
/**
|
||||
* For invoking back press to the parent activity
|
||||
*/
|
||||
@SuppressLint("ComposeCompositionLocalUsage")
|
||||
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
||||
|
||||
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
||||
|
||||
@@ -174,8 +174,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart
|
||||
) {
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
||||
SyncDataJob.startNow(this@App)
|
||||
}
|
||||
|
||||
@@ -199,7 +198,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun newImageLoader(context: Context): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
||||
@@ -239,8 +237,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume
|
||||
) {
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume) {
|
||||
SyncDataJob.startNow(this@App)
|
||||
}
|
||||
}
|
||||
@@ -339,7 +336,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
"""
|
||||
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
|
||||
Preview build: $syDebugVersion
|
||||
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
||||
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
||||
Android build ID: ${Build.DISPLAY}
|
||||
Device brand: ${Build.BRAND}
|
||||
Device manufacturer: ${Build.MANUFACTURER}
|
||||
|
||||
@@ -57,7 +57,7 @@ class BackupCreator(
|
||||
// SY -->
|
||||
private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(),
|
||||
private val getMergedManga: GetMergedManga = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get()
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
// SY <--
|
||||
) {
|
||||
|
||||
@@ -92,7 +92,7 @@ class BackupCreator(
|
||||
} else {
|
||||
emptyList()
|
||||
} + getMergedManga.await(), // SY <--
|
||||
options
|
||||
options,
|
||||
)
|
||||
val backup = Backup(
|
||||
backupManga = backupManga,
|
||||
|
||||
@@ -39,7 +39,8 @@ data class BackupOptions(
|
||||
// SY <--
|
||||
)
|
||||
|
||||
fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches
|
||||
fun canCreate() =
|
||||
libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches
|
||||
|
||||
companion object {
|
||||
val libraryOptions = persistentListOf(
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SavedSearchBackupCreator(
|
||||
private val handler: DatabaseHandler = Injekt.get()
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): List<BackupSavedSearch> {
|
||||
|
||||
@@ -3,12 +3,11 @@ package eu.kanade.tachiyomi.data.backup.models
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class Backup(
|
||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||
// @ProtoNumber(100) var backupBrokenSources, legacy source model with non-compliant proto number,
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||
|
||||
@@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class BackupChapter(
|
||||
// in 1.x some of these values have different names
|
||||
|
||||
@@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
class BackupExtensionRepos(
|
||||
@ProtoNumber(1) var baseUrl: String,
|
||||
|
||||
@@ -18,15 +18,3 @@ data class BackupHistory(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.")
|
||||
@Serializable
|
||||
data class BrokenBackupHistory(
|
||||
@ProtoNumber(0) var url: String,
|
||||
@ProtoNumber(1) var lastRead: Long,
|
||||
@ProtoNumber(2) var readDuration: Long = 0,
|
||||
) {
|
||||
fun toBackupHistory(): BackupHistory {
|
||||
return BackupHistory(url, lastRead, readDuration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@ package eu.kanade.tachiyomi.data.backup.models
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.manga.model.CustomMangaInfo
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Suppress(
|
||||
"DEPRECATION",
|
||||
"MagicNumber",
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
@Serializable
|
||||
data class BackupManga(
|
||||
// in 1.x some of these values have different names
|
||||
@@ -36,7 +32,7 @@ data class BackupManga(
|
||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||
@ProtoNumber(100) var favorite: Boolean = true,
|
||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||
// @ProtoNumber(102) var brokenHistory, legacy history model with non-compliant proto number
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ data class BackupMergedMangaReference(
|
||||
}
|
||||
|
||||
val backupMergedMangaReferenceMapper =
|
||||
{ _: Long,
|
||||
{
|
||||
_: Long,
|
||||
isInfoManga: Boolean,
|
||||
getChapterUpdates: Boolean,
|
||||
chapterSortMode: Long,
|
||||
|
||||
@@ -8,11 +8,3 @@ data class BackupSource(
|
||||
@ProtoNumber(1) var name: String = "",
|
||||
@ProtoNumber(2) var sourceId: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BrokenBackupSource(
|
||||
@ProtoNumber(0) var name: String = "",
|
||||
@ProtoNumber(1) var sourceId: Long,
|
||||
) {
|
||||
fun toBackupSource() = BackupSource(name, sourceId)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,20 @@ data class BackupTracking(
|
||||
}
|
||||
|
||||
val backupTrackMapper = {
|
||||
_: Long, _: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long ->
|
||||
_: Long,
|
||||
_: Long,
|
||||
syncId: Long,
|
||||
mediaId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastChapterRead: Double,
|
||||
totalChapters: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
->
|
||||
BackupTracking(
|
||||
syncId = syncId.toInt(),
|
||||
mediaId = mediaId,
|
||||
|
||||
@@ -74,7 +74,7 @@ class BackupRestorer(
|
||||
val backup = BackupDecoder(context).decode(uri)
|
||||
|
||||
// Store source mapping for error messages
|
||||
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
||||
val backupMaps = backup.backupSources
|
||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||
|
||||
if (options.libraryEntries) {
|
||||
@@ -200,7 +200,7 @@ class BackupRestorer(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreExtensionRepos(
|
||||
backupExtensionRepo: List<BackupExtensionRepos>
|
||||
backupExtensionRepo: List<BackupExtensionRepos>,
|
||||
) = launch {
|
||||
backupExtensionRepo
|
||||
.forEach {
|
||||
|
||||
@@ -27,7 +27,13 @@ data class RestoreOptions(
|
||||
// SY <--
|
||||
)
|
||||
|
||||
fun canRestore() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */
|
||||
fun canRestore() =
|
||||
libraryEntries ||
|
||||
categories ||
|
||||
appSettings ||
|
||||
extensionRepoSettings ||
|
||||
sourceSettings /* SY --> */ ||
|
||||
savedSearches /* SY <-- */
|
||||
|
||||
companion object {
|
||||
val options = persistentListOf(
|
||||
@@ -72,7 +78,7 @@ data class RestoreOptions(
|
||||
extensionRepoSettings = array[3],
|
||||
sourceSettings = array[4],
|
||||
// SY -->
|
||||
savedSearches = array[5]
|
||||
savedSearches = array[5],
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionRepoRestorer(
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get()
|
||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
@@ -32,7 +32,7 @@ class ExtensionRepoRestorer(
|
||||
backupRepo.name,
|
||||
backupRepo.shortName,
|
||||
backupRepo.website,
|
||||
backupRepo.signingKeyFingerprint
|
||||
backupRepo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ class MangaRestorer(
|
||||
chapters = backupManga.chapters,
|
||||
categories = backupManga.categories,
|
||||
backupCategories = backupCategories,
|
||||
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
|
||||
history = backupManga.history,
|
||||
tracks = backupManga.tracking,
|
||||
excludedScanlators = backupManga.excludedScanlators,
|
||||
// SY -->
|
||||
|
||||
@@ -36,8 +36,8 @@ class ChapterCache(
|
||||
private val context: Context,
|
||||
private val json: Json,
|
||||
// SY -->
|
||||
readerPreferences: ReaderPreferences
|
||||
//S Y <--
|
||||
readerPreferences: ReaderPreferences,
|
||||
// SY <--
|
||||
) {
|
||||
|
||||
// --> EH
|
||||
|
||||
@@ -45,7 +45,6 @@ import java.io.IOException
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class MangaCoverFetcher(
|
||||
private val url: String?,
|
||||
private val isLibraryManga: Boolean,
|
||||
@@ -86,7 +85,7 @@ class MangaCoverFetcher(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey
|
||||
diskCacheKey = diskCacheKey,
|
||||
),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
|
||||
@@ -58,7 +58,7 @@ class PagePreviewFetcher(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey
|
||||
diskCacheKey = diskCacheKey,
|
||||
),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
@@ -230,7 +230,7 @@ class PagePreviewFetcher(
|
||||
file = data,
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey,
|
||||
closeable = this
|
||||
closeable = this,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import coil3.ImageLoader
|
||||
@@ -11,37 +12,37 @@ import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.bitmapConfig
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
import mihon.core.common.archive.archiveReader
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedInputStream
|
||||
|
||||
/**
|
||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||
*/
|
||||
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
// SY -->
|
||||
var zip4j: ZipFile? = null
|
||||
var entry: FileHeader? = null
|
||||
|
||||
var coverStream: BufferedInputStream? = null
|
||||
if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) {
|
||||
if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) {
|
||||
zip4j = ZipFile(resources.file().toFile().absolutePath)
|
||||
entry = zip4j.fileHeaders.firstOrNull {
|
||||
it.fileName.equals(CbzCrypto.DEFAULT_COVER_NAME, ignoreCase = true)
|
||||
}
|
||||
|
||||
if (zip4j.isEncrypted) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
|
||||
coverStream = UniFile.fromFile(resources.file().toFile())
|
||||
?.archiveReader(context = context)
|
||||
?.getCoverStream()
|
||||
}
|
||||
}
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
zip4j.use { zipFile ->
|
||||
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile)
|
||||
coverStream.use { coverStream ->
|
||||
ImageDecoder.newInstance(coverStream ?: it.inputStream(), options.cropBorders, displayProfile)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class ChapterImpl : Chapter {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class TrackImpl : Track {
|
||||
|
||||
@@ -83,6 +83,11 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
context.stringResource(MR.strings.action_pause),
|
||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context),
|
||||
)
|
||||
addAction(
|
||||
R.drawable.ic_book_24dp,
|
||||
context.stringResource(MR.strings.action_show_manga),
|
||||
NotificationReceiver.openEntryPendingActivity(context, download.manga.id),
|
||||
)
|
||||
}
|
||||
|
||||
val downloadingProgressText = context.stringResource(
|
||||
@@ -160,9 +165,10 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*
|
||||
* @param reason the text to show.
|
||||
* @param timeout duration after which to automatically dismiss the notification.
|
||||
* @param mangaId the id of the entry being warned about
|
||||
* Only works on Android 8+.
|
||||
*/
|
||||
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null, mangaId: Long? = null) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title))
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||
@@ -170,6 +176,13 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
if (mangaId != null) {
|
||||
addAction(
|
||||
R.drawable.ic_book_24dp,
|
||||
context.stringResource(MR.strings.action_show_manga),
|
||||
NotificationReceiver.openEntryPendingActivity(context, mangaId),
|
||||
)
|
||||
}
|
||||
setProgress(0, 0, false)
|
||||
timeout?.let { setTimeoutAfter(it) }
|
||||
contentIntent?.let { setContentIntent(it) }
|
||||
@@ -187,8 +200,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*
|
||||
* @param error string containing error information.
|
||||
* @param chapter string containing chapter title.
|
||||
* @param mangaId the id of the entry that the error occurred on
|
||||
*/
|
||||
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
|
||||
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null, mangaId: Long? = null) {
|
||||
// Create notification
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(
|
||||
@@ -198,6 +212,13 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
if (mangaId != null) {
|
||||
addAction(
|
||||
R.drawable.ic_book_24dp,
|
||||
context.stringResource(MR.strings.action_show_manga),
|
||||
NotificationReceiver.openEntryPendingActivity(context, mangaId),
|
||||
)
|
||||
}
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
@@ -121,7 +121,8 @@ class DownloadProvider(
|
||||
getValidChapterDirNames(chp.name, chp.scanlator).any { dir ->
|
||||
mangaDir.findFile(dir) != null
|
||||
}
|
||||
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||
} == null ||
|
||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -44,10 +44,10 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import logcat.LogPriority
|
||||
import mihon.core.common.archive.ZipWriter
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.addFilesToZip
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNow
|
||||
@@ -65,12 +65,8 @@ import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
@@ -194,7 +190,7 @@ class Downloader(
|
||||
fun clearQueue() {
|
||||
cancelDownloaderJob()
|
||||
|
||||
_clearQueue()
|
||||
internalClearQueue()
|
||||
notifier.dismissProgress()
|
||||
}
|
||||
|
||||
@@ -208,9 +204,12 @@ class Downloader(
|
||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
.filter { it.status.value <= Download.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue
|
||||
// Ignore completed downloads, leave them in the queue
|
||||
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
||||
.groupBy { it.source }
|
||||
.toList().take(5) // Concurrently download from 5 different sources
|
||||
.toList()
|
||||
// Concurrently download from 5 different sources
|
||||
.take(5)
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
emit(activeDownloads)
|
||||
|
||||
@@ -337,6 +336,7 @@ class Downloader(
|
||||
context.stringResource(MR.strings.download_insufficient_space),
|
||||
download.chapter.name,
|
||||
download.manga.title,
|
||||
download.manga.id,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -426,7 +426,7 @@ class Downloader(
|
||||
// If the page list threw, it will resume here
|
||||
logcat(LogPriority.ERROR, error)
|
||||
download.status = Download.State.ERROR
|
||||
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||
notifier.onError(error.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ class Downloader(
|
||||
// Mark this page as error and allow to download the remaining
|
||||
page.progress = 0
|
||||
page.status = Page.State.ERROR
|
||||
notifier.onError(e.message, download.chapter.name, download.manga.title)
|
||||
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,70 +619,19 @@ class Downloader(
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
// SY -->
|
||||
if (CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet()) {
|
||||
archiveEncryptedChapter(mangaDir, dirname, tmpDir)
|
||||
return
|
||||
}
|
||||
val encrypt = CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet()
|
||||
// SY <--
|
||||
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
|
||||
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
||||
zipOut.setMethod(ZipEntry.STORED)
|
||||
|
||||
tmpDir.listFiles()?.forEach { img ->
|
||||
img.openInputStream().use { input ->
|
||||
val data = input.readBytes()
|
||||
val size = img.length()
|
||||
val entry = ZipEntry(img.name).apply {
|
||||
val crc = CRC32().apply {
|
||||
update(data)
|
||||
}
|
||||
setCrc(crc.value)
|
||||
|
||||
compressedSize = size
|
||||
setSize(size)
|
||||
}
|
||||
zipOut.putNextEntry(entry)
|
||||
zipOut.write(data)
|
||||
}
|
||||
ZipWriter(context, zip, /* SY --> */ encrypt /* SY <-- */).use { writer ->
|
||||
tmpDir.listFiles()?.forEach { file ->
|
||||
writer.write(file)
|
||||
}
|
||||
}
|
||||
zip.renameTo("$dirname.cbz")
|
||||
tmpDir.delete()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
|
||||
private fun archiveEncryptedChapter(
|
||||
mangaDir: UniFile,
|
||||
dirname: String,
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
tmpDir.filePath?.let { addPaddingToImage(File(it)) }
|
||||
|
||||
tmpDir.listFiles()?.toList()?.let { files ->
|
||||
mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
|
||||
}
|
||||
|
||||
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
|
||||
tmpDir.delete()
|
||||
}
|
||||
|
||||
private fun addPaddingToImage(imageDir: File) {
|
||||
imageDir.listFiles()
|
||||
// using ImageUtils isImage and findImageType functions causes IO errors when deleting files to set Exif Metadata
|
||||
// it should be safe to assume that all files with image extensions are actual images at this point
|
||||
?.filter {
|
||||
it.extension.equals("jpg", true) ||
|
||||
it.extension.equals("jpeg", true) ||
|
||||
it.extension.equals("png", true) ||
|
||||
it.extension.equals("webp", true)
|
||||
}
|
||||
?.forEach { ImageUtil.addPaddingToImageExif(it) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo.xml file inside the given directory.
|
||||
*/
|
||||
@@ -705,7 +654,7 @@ class Downloader(
|
||||
chapter,
|
||||
urls,
|
||||
categories,
|
||||
source.name
|
||||
source.name,
|
||||
)
|
||||
|
||||
// Remove the old file
|
||||
@@ -765,7 +714,7 @@ class Downloader(
|
||||
removeFromQueueIf { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
private fun _clearQueue() {
|
||||
private fun internalClearQueue() {
|
||||
_queueState.update {
|
||||
it.forEach { download ->
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
@@ -787,7 +736,7 @@ class Downloader(
|
||||
}
|
||||
|
||||
pause()
|
||||
_clearQueue()
|
||||
internalClearQueue()
|
||||
addAllToQueue(downloads)
|
||||
|
||||
if (wasRunning) {
|
||||
|
||||
@@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
@@ -58,17 +57,16 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.model.GroupLibraryMode
|
||||
import tachiyomi.domain.library.model.LibraryGroup
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
@@ -108,16 +106,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get()
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
private val getLibraryManga: GetLibraryManga = Injekt.get()
|
||||
private val getManga: GetManga = Injekt.get()
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val getCategories: GetCategories = Injekt.get()
|
||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||
private val fetchInterval: FetchInterval = Injekt.get()
|
||||
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
||||
|
||||
// SY -->
|
||||
private val getFavorites: GetFavorites = Injekt.get()
|
||||
@@ -363,14 +360,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
if (
|
||||
mdlistLogged && mangaInSource.firstOrNull()
|
||||
mdlistLogged &&
|
||||
mangaInSource.firstOrNull()
|
||||
?.let { it.manga.source in mangaDexSourceIds } == true
|
||||
) {
|
||||
launch {
|
||||
mangaInSource.forEach { (manga) ->
|
||||
try {
|
||||
val tracks = getTracks.await(manga.id)
|
||||
if (tracks.isEmpty() || tracks.none { it.trackerId == TrackerManager.MDLIST }) {
|
||||
if (tracks.isEmpty() ||
|
||||
tracks.none { it.trackerId == TrackerManager.MDLIST }
|
||||
) {
|
||||
val track = mdList.createInitialTracker(manga)
|
||||
insertTrack.await(mdList.refresh(track).toDomainTrack(false)!!)
|
||||
}
|
||||
@@ -400,10 +400,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
// SY -->
|
||||
.sortedByDescending { it.sourceOrder }.run {
|
||||
if (libraryPreferences.libraryReadDuplicateChapters().get()) {
|
||||
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read }
|
||||
val readChapters = getChaptersByMangaId.await(manga.id).filter {
|
||||
it.read
|
||||
}
|
||||
val newReadChapters = this.filter { chapter ->
|
||||
chapter.chapterNumber > 0 &&
|
||||
readChapters.any { it.chapterNumber == chapter.chapterNumber }
|
||||
readChapters.any {
|
||||
it.chapterNumber == chapter.chapterNumber
|
||||
}
|
||||
}
|
||||
|
||||
if (newReadChapters.isNotEmpty()) {
|
||||
@@ -415,12 +419,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
this
|
||||
}
|
||||
}
|
||||
//SY <--
|
||||
// SY <--
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
val categoryIds = getCategories.await(manga.id).map { it.id }
|
||||
if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters)
|
||||
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadChapters(manga, chaptersToDownload)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
@@ -766,7 +771,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else { NetworkType.CONNECTED },
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
},
|
||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
|
||||
@@ -265,6 +265,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
|
||||
|
||||
private const val ACTION_OPEN_ENTRY = "$ID.$NAME.ACTION_OPEN_ENTRY"
|
||||
|
||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||
@@ -503,6 +505,26 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that opens the manga info controller
|
||||
*
|
||||
* @param context context of application
|
||||
* @param mangaId id of the entry to open
|
||||
*/
|
||||
internal fun openEntryPendingActivity(context: Context, mangaId: Long): PendingIntent {
|
||||
val newIntent = Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(Constants.MANGA_EXTRA, mangaId)
|
||||
.putExtra("notificationId", mangaId.hashCode())
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
mangaId.hashCode(),
|
||||
newIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the library update
|
||||
*
|
||||
|
||||
@@ -147,7 +147,7 @@ class SyncManager(
|
||||
return
|
||||
}
|
||||
|
||||
if (remoteBackup === syncData.backup){
|
||||
if (remoteBackup === syncData.backup) {
|
||||
// nothing changed
|
||||
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
|
||||
@@ -72,7 +72,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||
try {
|
||||
val remoteSData = pullSyncData()
|
||||
|
||||
if (remoteSData != null ){
|
||||
if (remoteSData != null) {
|
||||
// Get local unique device ID
|
||||
val localDeviceId = syncPreferences.uniqueDeviceID()
|
||||
val lastSyncDeviceId = remoteSData.deviceId
|
||||
@@ -86,7 +86,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||
return if (lastSyncDeviceId == localDeviceId) {
|
||||
pushSyncData(syncData)
|
||||
syncData.backup
|
||||
}else{
|
||||
} else {
|
||||
// Merge the local and remote sync data
|
||||
val mergedSyncData = mergeSyncData(syncData, remoteSData)
|
||||
pushSyncData(mergedSyncData)
|
||||
@@ -165,7 +165,9 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||
appProperties = mapOf("deviceId" to syncData.deviceId)
|
||||
}
|
||||
drive.files().update(fileId, fileMetadata, mediaContent).execute()
|
||||
logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" }
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updated existing sync data file in Google Drive with file ID: $fileId"
|
||||
}
|
||||
} else {
|
||||
val fileMetadata = File().apply {
|
||||
name = remoteFileName
|
||||
@@ -176,7 +178,9 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||
val uploadedFile = drive.files().create(fileMetadata, mediaContent)
|
||||
.setFields("id")
|
||||
.execute()
|
||||
logcat(LogPriority.DEBUG) { "Created new sync data file in Google Drive with file ID: ${uploadedFile.id}" }
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Created new sync data file in Google Drive with file ID: ${uploadedFile.id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +207,6 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus {
|
||||
val drive = googleDriveService.driveService
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ abstract class SyncService(
|
||||
val json: Json,
|
||||
val syncPreferences: SyncPreferences,
|
||||
) {
|
||||
abstract suspend fun doSync(syncData: SyncData): Backup?;
|
||||
abstract suspend fun doSync(syncData: SyncData): Backup?
|
||||
|
||||
/**
|
||||
* Merges the local and remote sync data into a single JSON string.
|
||||
@@ -44,7 +44,8 @@ abstract class SyncService(
|
||||
remoteSyncData.backup?.backupManga,
|
||||
localSyncData.backup?.backupCategories ?: emptyList(),
|
||||
remoteSyncData.backup?.backupCategories ?: emptyList(),
|
||||
mergedCategoriesList)
|
||||
mergedCategoriesList,
|
||||
)
|
||||
|
||||
val mergedSourcesList =
|
||||
mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources)
|
||||
@@ -120,11 +121,13 @@ abstract class SyncService(
|
||||
val mergedCategoriesMapByName = mergedCategories.associateBy { it.name }
|
||||
|
||||
fun updateCategories(theManga: BackupManga, theMap: Map<Long, BackupCategory>): BackupManga {
|
||||
return theManga.copy(categories = theManga.categories.mapNotNull {
|
||||
theMap[it]?.let { category ->
|
||||
mergedCategoriesMapByName[category.name]?.order
|
||||
}
|
||||
})
|
||||
return theManga.copy(
|
||||
categories = theManga.categories.mapNotNull {
|
||||
theMap[it]?.let { category ->
|
||||
mergedCategoriesMapByName[category.name]?.order
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
@@ -147,7 +150,7 @@ abstract class SyncService(
|
||||
}
|
||||
updateCategories(
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
localCategoriesMapByOrder
|
||||
localCategoriesMapByOrder,
|
||||
)
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
@@ -155,7 +158,7 @@ abstract class SyncService(
|
||||
}
|
||||
updateCategories(
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
remoteCategoriesMapByOrder
|
||||
remoteCategoriesMapByOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -301,7 +304,7 @@ abstract class SyncService(
|
||||
|
||||
private fun mergeSourcesLists(
|
||||
localSources: List<BackupSource>?,
|
||||
remoteSources: List<BackupSource>?
|
||||
remoteSources: List<BackupSource>?,
|
||||
): List<BackupSource> {
|
||||
val logTag = "MergeSources"
|
||||
|
||||
@@ -346,7 +349,7 @@ abstract class SyncService(
|
||||
|
||||
private fun mergePreferencesLists(
|
||||
localPreferences: List<BackupPreference>?,
|
||||
remotePreferences: List<BackupPreference>?
|
||||
remotePreferences: List<BackupPreference>?,
|
||||
): List<BackupPreference> {
|
||||
val logTag = "MergePreferences"
|
||||
|
||||
@@ -394,7 +397,7 @@ abstract class SyncService(
|
||||
|
||||
private fun mergeSourcePreferencesLists(
|
||||
localPreferences: List<BackupSourcePreferences>?,
|
||||
remotePreferences: List<BackupSourcePreferences>?
|
||||
remotePreferences: List<BackupSourcePreferences>?,
|
||||
): List<BackupSourcePreferences> {
|
||||
val logTag = "MergeSourcePreferences"
|
||||
|
||||
@@ -408,38 +411,39 @@ abstract class SyncService(
|
||||
}
|
||||
|
||||
// Merge both source preferences maps
|
||||
val mergedSourcePreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct().mapNotNull { sourceKey ->
|
||||
val localSourcePreference = localPreferencesMap[sourceKey]
|
||||
val remoteSourcePreference = remotePreferencesMap[sourceKey]
|
||||
val mergedSourcePreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct()
|
||||
.mapNotNull { sourceKey ->
|
||||
val localSourcePreference = localPreferencesMap[sourceKey]
|
||||
val remoteSourcePreference = remotePreferencesMap[sourceKey]
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Processing source preference key: $sourceKey. " +
|
||||
"Local source preference: ${localSourcePreference != null}, " +
|
||||
"Remote source preference: ${remoteSourcePreference != null}"
|
||||
}
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Processing source preference key: $sourceKey. " +
|
||||
"Local source preference: ${localSourcePreference != null}, " +
|
||||
"Remote source preference: ${remoteSourcePreference != null}"
|
||||
}
|
||||
|
||||
when {
|
||||
localSourcePreference != null && remoteSourcePreference == null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Using local source preference: ${localSourcePreference.sourceKey}."
|
||||
when {
|
||||
localSourcePreference != null && remoteSourcePreference == null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Using local source preference: ${localSourcePreference.sourceKey}."
|
||||
}
|
||||
localSourcePreference
|
||||
}
|
||||
localSourcePreference
|
||||
}
|
||||
remoteSourcePreference != null && localSourcePreference == null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Using remote source preference: ${remoteSourcePreference.sourceKey}."
|
||||
remoteSourcePreference != null && localSourcePreference == null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Using remote source preference: ${remoteSourcePreference.sourceKey}."
|
||||
}
|
||||
remoteSourcePreference
|
||||
}
|
||||
remoteSourcePreference
|
||||
localSourcePreference != null && remoteSourcePreference != null -> {
|
||||
// Merge the individual preferences within the source preferences
|
||||
val mergedPrefs =
|
||||
mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs)
|
||||
BackupSourcePreferences(sourceKey, mergedPrefs)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
localSourcePreference != null && remoteSourcePreference != null -> {
|
||||
// Merge the individual preferences within the source preferences
|
||||
val mergedPrefs =
|
||||
mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs)
|
||||
BackupSourcePreferences(sourceKey, mergedPrefs)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Source preferences merge completed. Total merged source preferences: ${mergedSourcePreferences.size}"
|
||||
@@ -450,7 +454,7 @@ abstract class SyncService(
|
||||
|
||||
private fun mergeIndividualPreferences(
|
||||
localPrefs: List<BackupPreference>,
|
||||
remotePrefs: List<BackupPreference>
|
||||
remotePrefs: List<BackupPreference>,
|
||||
): List<BackupPreference> {
|
||||
val mergedPrefsMap = (localPrefs + remotePrefs).associateBy { it.key }
|
||||
return mergedPrefsMap.values.toList()
|
||||
@@ -459,7 +463,7 @@ abstract class SyncService(
|
||||
// SY -->
|
||||
private fun mergeSavedSearchesLists(
|
||||
localSearches: List<BackupSavedSearch>?,
|
||||
remoteSearches: List<BackupSavedSearch>?
|
||||
remoteSearches: List<BackupSavedSearch>?,
|
||||
): List<BackupSavedSearch> {
|
||||
val logTag = "MergeSavedSearches"
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class SyncYomiSyncService(
|
||||
try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData = if (remoteData != null){
|
||||
val finalSyncData = if (remoteData != null) {
|
||||
assert(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
logcat(LogPriority.DEBUG, "SyncService") {
|
||||
"Try update remote data with ETag($etag)"
|
||||
@@ -54,7 +54,6 @@ class SyncYomiSyncService(
|
||||
|
||||
pushSyncData(finalSyncData, etag)
|
||||
return finalSyncData.backup
|
||||
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||
notifier.showSyncError(e.message)
|
||||
@@ -113,7 +112,6 @@ class SyncYomiSyncService(
|
||||
// return default value so we can overwrite it
|
||||
Pair(null, "")
|
||||
}
|
||||
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to download sync data: $responseBody")
|
||||
@@ -165,11 +163,9 @@ class SyncYomiSyncService(
|
||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.lastSyncEtag().set(newETag)
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||
|
||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||
// other clients updated remote data, will try next time
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||
|
||||
@@ -47,10 +47,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
@@ -65,7 +65,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
@@ -109,7 +109,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("completedAt", createDate(track.finished_reading_date))
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
track
|
||||
}
|
||||
@@ -119,9 +119,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
withIOContext {
|
||||
val query = """
|
||||
|mutation DeleteManga(${'$'}listId: Int) {
|
||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||
|deleted
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
@@ -131,7 +131,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("listId", track.libraryId)
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
@@ -242,7 +242,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
@@ -286,7 +286,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
@@ -364,17 +364,17 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId = "16329"
|
||||
private const val apiUrl = "https://graphql.anilist.co/"
|
||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||
private const val CLIENT_ID = "16329"
|
||||
private const val API_URL = "https://graphql.anilist.co/"
|
||||
private const val BASE_URL = "https://anilist.co/api/v2/"
|
||||
private const val BASE_MANGA_URL = "https://anilist.co/manga/"
|
||||
|
||||
fun mangaUrl(mediaId: Long): String {
|
||||
return baseMangaUrl + mediaId
|
||||
return BASE_MANGA_URL + mediaId
|
||||
}
|
||||
|
||||
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
fun authUrl(): Uri = "${BASE_URL}oauth/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", CLIENT_ID)
|
||||
.appendQueryParameter("response_type", "token")
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class BangumiApi(
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body))
|
||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
|
||||
.awaitSuccess()
|
||||
track
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class BangumiApi(
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody))
|
||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
|
||||
.awaitSuccess()
|
||||
|
||||
// chapter update
|
||||
@@ -64,7 +64,7 @@ class BangumiApi(
|
||||
.build()
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$apiUrl/subject/${track.remote_id}/update/watched_eps",
|
||||
"$API_URL/subject/${track.remote_id}/update/watched_eps",
|
||||
body = body,
|
||||
),
|
||||
).awaitSuccess()
|
||||
@@ -75,7 +75,7 @@ class BangumiApi(
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
||||
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
@@ -124,7 +124,7 @@ class BangumiApi(
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$apiUrl/subject/${track.remote_id}"))
|
||||
authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
.let { jsonToSearch(it) }
|
||||
@@ -134,7 +134,7 @@ class BangumiApi(
|
||||
|
||||
suspend fun statusLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
val urlUserRead = "$apiUrl/collection/${track.remote_id}"
|
||||
val urlUserRead = "$API_URL/collection/${track.remote_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
@@ -171,41 +171,41 @@ class BangumiApi(
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
oauthUrl,
|
||||
OAUTH_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.add("redirect_uri", REDIRECT_URL)
|
||||
.build(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val clientId = "bgm291665acbd06a4c28"
|
||||
private const val clientSecret = "43e5ce36b207de16e5d3cfd3e79118db"
|
||||
private const val CLIENT_ID = "bgm291665acbd06a4c28"
|
||||
private const val CLIENT_SECRET = "43e5ce36b207de16e5d3cfd3e79118db"
|
||||
|
||||
private const val apiUrl = "https://api.bgm.tv"
|
||||
private const val oauthUrl = "https://bgm.tv/oauth/access_token"
|
||||
private const val loginUrl = "https://bgm.tv/oauth/authorize"
|
||||
private const val API_URL = "https://api.bgm.tv"
|
||||
private const val OAUTH_URL = "https://bgm.tv/oauth/access_token"
|
||||
private const val LOGIN_URL = "https://bgm.tv/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "mihon://bangumi-auth"
|
||||
private const val REDIRECT_URL = "mihon://bangumi-auth"
|
||||
|
||||
fun authUrl(): Uri =
|
||||
loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
LOGIN_URL.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", CLIENT_ID)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("redirect_uri", REDIRECT_URL)
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
oauthUrl,
|
||||
OAUTH_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("refresh_token", token)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.add("redirect_uri", REDIRECT_URL)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"${baseUrl}library-entries",
|
||||
"${BASE_URL}library-entries",
|
||||
headers = headersOf(
|
||||
"Content-Type",
|
||||
"application/vnd.api+json",
|
||||
@@ -104,7 +104,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
Request.Builder()
|
||||
.url("${baseUrl}library-entries/${track.remote_id}")
|
||||
.url("${BASE_URL}library-entries/${track.remote_id}")
|
||||
.headers(
|
||||
headersOf(
|
||||
"Content-Type",
|
||||
@@ -130,7 +130,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
authClient
|
||||
.newCall(
|
||||
DELETE(
|
||||
"${baseUrl}library-entries/${track.remoteId}",
|
||||
"${BASE_URL}library-entries/${track.remoteId}",
|
||||
headers = headersOf(
|
||||
"Content-Type",
|
||||
"application/vnd.api+json",
|
||||
@@ -143,7 +143,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET(algoliaKeyUrl))
|
||||
authClient.newCall(GET(ALGOLIA_KEY_URL))
|
||||
.awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
@@ -157,16 +157,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val jsonObject = buildJsonObject {
|
||||
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter")
|
||||
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$ALGOLIA_FILTER")
|
||||
}
|
||||
|
||||
with(json) {
|
||||
client.newCall(
|
||||
POST(
|
||||
algoliaUrl,
|
||||
ALGOLIA_URL,
|
||||
headers = headersOf(
|
||||
"X-Algolia-Application-Id",
|
||||
algoliaAppId,
|
||||
ALGOLIA_APP_ID,
|
||||
"X-Algolia-API-Key",
|
||||
key,
|
||||
),
|
||||
@@ -187,7 +187,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
|
||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
||||
.encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
@@ -210,7 +210,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
|
||||
suspend fun getLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
||||
.encodedQuery("filter[id]=${track.remote_id}")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
@@ -237,11 +237,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
.add("username", username)
|
||||
.add("password", password)
|
||||
.add("grant_type", "password")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.build()
|
||||
with(json) {
|
||||
client.newCall(POST(loginUrl, body = formBody))
|
||||
client.newCall(POST(LOGIN_URL, body = formBody))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
@@ -250,7 +250,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
|
||||
suspend fun getCurrentUser(): String {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}users".toUri().buildUpon()
|
||||
val url = "${BASE_URL}users".toUri().buildUpon()
|
||||
.encodedQuery("filter[self]=true")
|
||||
.build()
|
||||
with(json) {
|
||||
@@ -265,35 +265,31 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId =
|
||||
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val clientSecret =
|
||||
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
|
||||
private const val baseUrl = "https://kitsu.app/api/edge/"
|
||||
private const val loginUrl = "https://kitsu.app/api/oauth/token"
|
||||
private const val baseMangaUrl = "https://kitsu.app/manga/"
|
||||
private const val algoliaKeyUrl = "https://kitsu.app/api/edge/algolia-keys/media/"
|
||||
private const val BASE_URL = "https://kitsu.app/api/edge/"
|
||||
private const val LOGIN_URL = "https://kitsu.app/api/oauth/token"
|
||||
private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
|
||||
private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"
|
||||
|
||||
private const val algoliaUrl =
|
||||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
||||
private const val algoliaAppId = "AWQO5J657S"
|
||||
private const val algoliaFilter =
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" +
|
||||
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
||||
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
private const val ALGOLIA_APP_ID = "AWQO5J657S"
|
||||
private const val ALGOLIA_URL = "https://$ALGOLIA_APP_ID-dsn.algolia.net/1/indexes/production_media/query/"
|
||||
private const val ALGOLIA_FILTER = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" +
|
||||
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
||||
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
fun mangaUrl(remoteId: Long): String {
|
||||
return baseMangaUrl + remoteId
|
||||
return BASE_MANGA_URL + remoteId
|
||||
}
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
loginUrl,
|
||||
LOGIN_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("refresh_token", token)
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import java.util.Locale
|
||||
|
||||
object KitsuDateHelper {
|
||||
|
||||
private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH)
|
||||
private const val PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
private val formatter = SimpleDateFormat(PATTERN, Locale.ENGLISH)
|
||||
|
||||
fun convert(dateValue: Long): String? {
|
||||
if (dateValue == 0L) return null
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
|
||||
package eu.kanade.tachiyomi.data.track.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
@@ -54,7 +54,7 @@ class ShikimoriApi(
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$apiUrl/v2/user_rates",
|
||||
"$API_URL/v2/user_rates",
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
).awaitSuccess()
|
||||
@@ -72,14 +72,14 @@ class ShikimoriApi(
|
||||
suspend fun deleteLibManga(track: DomainTrack) {
|
||||
withIOContext {
|
||||
authClient
|
||||
.newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}"))
|
||||
.newCall(DELETE("$API_URL/v2/user_rates/${track.libraryId}"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||
val url = "$API_URL/mangas".toUri().buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
@@ -102,10 +102,10 @@ class ShikimoriApi(
|
||||
remote_id = obj["id"]!!.jsonPrimitive.long
|
||||
title = obj["name"]!!.jsonPrimitive.content
|
||||
total_chapters = obj["chapters"]!!.jsonPrimitive.long
|
||||
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
|
||||
cover_url = BASE_URL + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
|
||||
summary = ""
|
||||
score = obj["score"]!!.jsonPrimitive.double
|
||||
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
|
||||
tracking_url = BASE_URL + obj["url"]!!.jsonPrimitive.content
|
||||
publishing_status = obj["status"]!!.jsonPrimitive.content
|
||||
publishing_type = obj["kind"]!!.jsonPrimitive.content
|
||||
start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: ""
|
||||
@@ -121,13 +121,13 @@ class ShikimoriApi(
|
||||
last_chapter_read = obj["chapters"]!!.jsonPrimitive.double
|
||||
score = obj["score"]!!.jsonPrimitive.int.toDouble()
|
||||
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
|
||||
tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content
|
||||
tracking_url = BASE_URL + mangas["url"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||
return withIOContext {
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
val urlMangas = "$API_URL/mangas".toUri().buildUpon()
|
||||
.appendPath(track.remote_id.toString())
|
||||
.build()
|
||||
val mangas = with(json) {
|
||||
@@ -136,7 +136,7 @@ class ShikimoriApi(
|
||||
.parseAs<JsonObject>()
|
||||
}
|
||||
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
val url = "$API_URL/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", userId)
|
||||
.appendQueryParameter("target_id", track.remote_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
@@ -160,7 +160,7 @@ class ShikimoriApi(
|
||||
|
||||
suspend fun getCurrentUser(): Int {
|
||||
return with(json) {
|
||||
authClient.newCall(GET("$apiUrl/users/whoami"))
|
||||
authClient.newCall(GET("$API_URL/users/whoami"))
|
||||
.awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
@@ -180,39 +180,39 @@ class ShikimoriApi(
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
oauthUrl,
|
||||
OAUTH_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.add("redirect_uri", REDIRECT_URL)
|
||||
.build(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val clientId = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA"
|
||||
private const val clientSecret = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus"
|
||||
private const val CLIENT_ID = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA"
|
||||
private const val CLIENT_SECRET = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus"
|
||||
|
||||
private const val baseUrl = "https://shikimori.one"
|
||||
private const val apiUrl = "$baseUrl/api"
|
||||
private const val oauthUrl = "$baseUrl/oauth/token"
|
||||
private const val loginUrl = "$baseUrl/oauth/authorize"
|
||||
private const val BASE_URL = "https://shikimori.one"
|
||||
private const val API_URL = "$BASE_URL/api"
|
||||
private const val OAUTH_URL = "$BASE_URL/oauth/token"
|
||||
private const val LOGIN_URL = "$BASE_URL/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "mihon://shikimori-auth"
|
||||
private const val REDIRECT_URL = "mihon://shikimori-auth"
|
||||
|
||||
fun authUrl(): Uri = loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
fun authUrl(): Uri = LOGIN_URL.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", CLIENT_ID)
|
||||
.appendQueryParameter("redirect_uri", REDIRECT_URL)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
oauthUrl,
|
||||
OAUTH_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("refresh_token", token)
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -69,17 +69,18 @@ class ExtensionManager(
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||
val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope)
|
||||
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensions(scope)
|
||||
|
||||
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||
|
||||
private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||
// SY -->
|
||||
val availableExtensionsFlow = _availableExtensionsMapFlow.map { it.filterNotBlacklisted().values.toList() }
|
||||
.stateIn(scope, SharingStarted.Lazily, _availableExtensionsMapFlow.value.values.toList())
|
||||
val availableExtensionsFlow = availableExtensionMapFlow.map { it.filterNotBlacklisted().values.toList() }
|
||||
.stateIn(scope, SharingStarted.Lazily, availableExtensionMapFlow.value.values.toList())
|
||||
// SY <--
|
||||
|
||||
private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||
val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope)
|
||||
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensions(scope)
|
||||
|
||||
init {
|
||||
initExtensions()
|
||||
@@ -89,7 +90,7 @@ class ExtensionManager(
|
||||
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
|
||||
|
||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||
val pkgName = _installedExtensionsMapFlow.value.values
|
||||
val pkgName = installedExtensionMapFlow.value.values
|
||||
.find { ext ->
|
||||
ext.sources.any { it.id == sourceId }
|
||||
}
|
||||
@@ -128,11 +129,11 @@ class ExtensionManager(
|
||||
private fun initExtensions() {
|
||||
val extensions = ExtensionLoader.loadExtensions(context)
|
||||
|
||||
_installedExtensionsMapFlow.value = extensions
|
||||
installedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
|
||||
_untrustedExtensionsMapFlow.value = extensions
|
||||
untrustedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
// SY -->
|
||||
@@ -159,7 +160,7 @@ class ExtensionManager(
|
||||
// EXH <--
|
||||
|
||||
/**
|
||||
* Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow].
|
||||
* Finds the available extensions in the [api] and updates [availableExtensionMapFlow].
|
||||
*/
|
||||
suspend fun findAvailableExtensions() {
|
||||
val extensions: List<Extension.Available> = try {
|
||||
@@ -172,7 +173,7 @@ class ExtensionManager(
|
||||
|
||||
enableAdditionalSubLanguages(extensions)
|
||||
|
||||
_availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName }
|
||||
availableExtensionMapFlow.value = extensions.associateBy { it.pkgName }
|
||||
updatedInstalledExtensionsStatuses(extensions)
|
||||
setupAvailableExtensionsSourcesDataMap(extensions)
|
||||
}
|
||||
@@ -218,7 +219,7 @@ class ExtensionManager(
|
||||
return
|
||||
}
|
||||
|
||||
val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap()
|
||||
val installedExtensionsMap = installedExtensionMapFlow.value.toMutableMap()
|
||||
var changed = false
|
||||
for ((pkgName, extension) in installedExtensionsMap) {
|
||||
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
||||
@@ -247,7 +248,7 @@ class ExtensionManager(
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
_installedExtensionsMapFlow.value = installedExtensionsMap
|
||||
installedExtensionMapFlow.value = installedExtensionsMap
|
||||
}
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
@@ -271,7 +272,7 @@ class ExtensionManager(
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
||||
val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow()
|
||||
val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
|
||||
return installExtension(availableExt)
|
||||
}
|
||||
|
||||
@@ -308,11 +309,11 @@ class ExtensionManager(
|
||||
* @param extension the extension to trust
|
||||
*/
|
||||
suspend fun trust(extension: Extension.Untrusted) {
|
||||
_untrustedExtensionsMapFlow.value[extension.pkgName] ?: return
|
||||
untrustedExtensionMapFlow.value[extension.pkgName] ?: return
|
||||
|
||||
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
|
||||
|
||||
_untrustedExtensionsMapFlow.value -= extension.pkgName
|
||||
untrustedExtensionMapFlow.value -= extension.pkgName
|
||||
|
||||
ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
|
||||
.let { it as? LoadResult.Success }
|
||||
@@ -332,7 +333,7 @@ class ExtensionManager(
|
||||
}
|
||||
// SY <--
|
||||
|
||||
_installedExtensionsMapFlow.value += extension
|
||||
installedExtensionMapFlow.value += extension
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -349,7 +350,7 @@ class ExtensionManager(
|
||||
}
|
||||
// SY <--
|
||||
|
||||
_installedExtensionsMapFlow.value += extension
|
||||
installedExtensionMapFlow.value += extension
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,8 +360,8 @@ class ExtensionManager(
|
||||
* @param pkgName The package name of the uninstalled application.
|
||||
*/
|
||||
private fun unregisterExtension(pkgName: String) {
|
||||
_installedExtensionsMapFlow.value -= pkgName
|
||||
_untrustedExtensionsMapFlow.value -= pkgName
|
||||
installedExtensionMapFlow.value -= pkgName
|
||||
untrustedExtensionMapFlow.value -= pkgName
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,8 +380,8 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
||||
_installedExtensionsMapFlow.value -= extension.pkgName
|
||||
_untrustedExtensionsMapFlow.value += extension
|
||||
installedExtensionMapFlow.value -= extension.pkgName
|
||||
untrustedExtensionMapFlow.value += extension
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
@@ -404,14 +405,14 @@ class ExtensionManager(
|
||||
|
||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||
val availableExt = availableExtension
|
||||
?: _availableExtensionsMapFlow.value[pkgName]
|
||||
?: availableExtensionMapFlow.value[pkgName]
|
||||
?: return false
|
||||
|
||||
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
||||
}
|
||||
|
||||
private fun updatePendingUpdatesCount() {
|
||||
val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate }
|
||||
val pendingUpdateCount = installedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
preferences.extensionUpdatesCount().set(pendingUpdateCount)
|
||||
if (pendingUpdateCount == 0) {
|
||||
ExtensionUpdateNotifier(context).dismiss()
|
||||
|
||||
@@ -34,8 +34,10 @@ internal class ExtensionApi {
|
||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
// SY -->
|
||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||
|
||||
// SY <--
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
|
||||
@@ -3,15 +3,20 @@ package eu.kanade.tachiyomi.extension.api
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notify
|
||||
import tachiyomi.core.common.i18n.pluralStringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionUpdateNotifier(private val context: Context) {
|
||||
|
||||
class ExtensionUpdateNotifier(
|
||||
private val context: Context,
|
||||
private val securityPreferences: SecurityPreferences = Injekt.get(),
|
||||
) {
|
||||
fun promptUpdates(names: List<String>) {
|
||||
context.notify(
|
||||
Notifications.ID_UPDATES_TO_EXTS,
|
||||
@@ -24,9 +29,11 @@ class ExtensionUpdateNotifier(private val context: Context) {
|
||||
names.size,
|
||||
),
|
||||
)
|
||||
val extNames = names.joinToString(", ")
|
||||
setContentText(extNames)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
||||
if (!securityPreferences.hideNotificationContent().get()) {
|
||||
val extNames = names.joinToString(", ")
|
||||
setContentText(extNames)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
||||
}
|
||||
setSmallIcon(R.drawable.ic_extension_24dp)
|
||||
setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context))
|
||||
setAutoCancel(true)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
enum class InstallStep {
|
||||
Idle, Pending, Downloading, Installing, Installed, Error;
|
||||
Idle,
|
||||
Pending,
|
||||
Downloading,
|
||||
Installing,
|
||||
Installed,
|
||||
Error,
|
||||
;
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return this == Installed || this == Error || this == Idle
|
||||
|
||||
@@ -223,7 +223,6 @@ internal object ExtensionLoader {
|
||||
* @param context The application context.
|
||||
* @param extensionInfo The extension to load.
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount")
|
||||
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
||||
val pkgManager = context.packageManager
|
||||
val pkgInfo = extensionInfo.packageInfo
|
||||
|
||||
@@ -383,7 +383,7 @@ class EHentai(
|
||||
doc.select("#gdd .gdt1").find { el ->
|
||||
el.text().lowercase() == "posted:"
|
||||
}!!.nextElementSibling()!!.text(),
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
|
||||
)!!.toInstant().toEpochMilli(),
|
||||
scanlator = EHentaiSearchMetadata.galleryId(location),
|
||||
)
|
||||
@@ -401,7 +401,7 @@ class EHentai(
|
||||
chapter_number = index + 2f,
|
||||
date_upload = ZonedDateTime.parse(
|
||||
posted,
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
|
||||
).toInstant().toEpochMilli(),
|
||||
scanlator = EHentaiSearchMetadata.galleryId(link),
|
||||
)
|
||||
@@ -542,9 +542,10 @@ class EHentai(
|
||||
if (
|
||||
MATCH_SEEK_REGEX.matches(jumpSeekValue) ||
|
||||
(
|
||||
MATCH_YEAR_REGEX.matches(jumpSeekValue) && jumpSeekValue.toIntOrNull()?.let {
|
||||
it in 2007..2099
|
||||
} == true
|
||||
MATCH_YEAR_REGEX.matches(jumpSeekValue) &&
|
||||
jumpSeekValue.toIntOrNull()?.let {
|
||||
it in 2007..2099
|
||||
} == true
|
||||
)
|
||||
) {
|
||||
uri.appendQueryParameter("seek", jumpSeekValue)
|
||||
@@ -715,7 +716,7 @@ class EHentai(
|
||||
when (left.removeSuffix(":").lowercase()) {
|
||||
"posted" -> datePosted = ZonedDateTime.parse(
|
||||
right,
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
|
||||
).toInstant().toEpochMilli()
|
||||
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
|
||||
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
|
||||
|
||||
@@ -87,6 +87,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
||||
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
||||
private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty()
|
||||
private fun coverQuality() = sourcePreferences.getString(getCoverQualityPrefKey(mdLang.lang), "").orEmpty()
|
||||
private fun tryUsingFirstVolumeCover() = sourcePreferences.getBoolean(getTryUsingFirstVolumeCoverKey(mdLang.lang), false)
|
||||
|
||||
private val mangadexService by lazy {
|
||||
MangaDexService(client)
|
||||
@@ -189,11 +191,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return mangaHandler.fetchMangaDetailsObservable(manga, id)
|
||||
return mangaHandler.fetchMangaDetailsObservable(manga, id, coverQuality(), tryUsingFirstVolumeCover())
|
||||
}
|
||||
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return mangaHandler.getMangaDetails(manga, id)
|
||||
return mangaHandler.getMangaDetails(manga, id, coverQuality(), tryUsingFirstVolumeCover())
|
||||
}
|
||||
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
@@ -239,7 +241,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
override fun newMetaInstance() = MangaDexSearchMetadata()
|
||||
|
||||
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) {
|
||||
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third)
|
||||
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third, null, coverQuality())
|
||||
}
|
||||
|
||||
// LoginSource methods
|
||||
@@ -334,5 +336,17 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
fun getBlockedUploaderPrefKey(dexLang: String): String {
|
||||
return "${blockedUploaderPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val coverQualityPref = "thumbnailQuality"
|
||||
|
||||
fun getCoverQualityPrefKey(dexLang: String): String {
|
||||
return "${coverQualityPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val tryUsingFirstVolumeCover = "tryUsingFirstVolumeCover"
|
||||
|
||||
fun getTryUsingFirstVolumeCoverKey(dexLang: String): String {
|
||||
return "${tryUsingFirstVolumeCover}_$dexLang"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
@@ -19,6 +18,7 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
@@ -42,6 +42,7 @@ class MergedSource : HttpSource() {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
private val downloadPreferences: DownloadPreferences by injectLazy()
|
||||
private val filterChaptersForDownload: FilterChaptersForDownload by injectLazy()
|
||||
|
||||
override val id: Long = MERGED_SOURCE_ID
|
||||
|
||||
@@ -119,12 +120,6 @@ class MergedSource : HttpSource() {
|
||||
"Manga references are empty, chapters unavailable, merge is likely corrupted"
|
||||
}
|
||||
|
||||
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(
|
||||
getCategories.await(manga.id).map {
|
||||
it.id
|
||||
},
|
||||
downloadPreferences,
|
||||
)
|
||||
val semaphore = Semaphore(5)
|
||||
var exception: Exception? = null
|
||||
return supervisorScope {
|
||||
@@ -141,11 +136,15 @@ class MergedSource : HttpSource() {
|
||||
val chapterList = source.getChapterList(loadedManga.toSManga())
|
||||
val results =
|
||||
syncChaptersWithSource.await(chapterList, loadedManga, source)
|
||||
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||
downloadManager.downloadChapters(
|
||||
loadedManga,
|
||||
results,
|
||||
)
|
||||
|
||||
if (reference.downloadChapters) {
|
||||
val chaptersToDownload = filterChaptersForDownload.await(manga, results)
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadManager.downloadChapters(
|
||||
loadedManga,
|
||||
chaptersToDownload,
|
||||
)
|
||||
}
|
||||
}
|
||||
results
|
||||
} else {
|
||||
|
||||
@@ -73,16 +73,17 @@ interface SecureActivityDelegate {
|
||||
}
|
||||
|
||||
val lockedDays = preferences.authenticatorDays().get()
|
||||
val canLockToday = lockedDays == LOCK_ALL_DAYS || when (today.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.SUNDAY -> (lockedDays and LOCK_SUNDAY) == LOCK_SUNDAY
|
||||
Calendar.MONDAY -> (lockedDays and LOCK_MONDAY) == LOCK_MONDAY
|
||||
Calendar.TUESDAY -> (lockedDays and LOCK_TUESDAY) == LOCK_TUESDAY
|
||||
Calendar.WEDNESDAY -> (lockedDays and LOCK_WEDNESDAY) == LOCK_WEDNESDAY
|
||||
Calendar.THURSDAY -> (lockedDays and LOCK_THURSDAY) == LOCK_THURSDAY
|
||||
Calendar.FRIDAY -> (lockedDays and LOCK_FRIDAY) == LOCK_FRIDAY
|
||||
Calendar.SATURDAY -> (lockedDays and LOCK_SATURDAY) == LOCK_SATURDAY
|
||||
else -> false
|
||||
}
|
||||
val canLockToday = lockedDays == LOCK_ALL_DAYS ||
|
||||
when (today.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.SUNDAY -> (lockedDays and LOCK_SUNDAY) == LOCK_SUNDAY
|
||||
Calendar.MONDAY -> (lockedDays and LOCK_MONDAY) == LOCK_MONDAY
|
||||
Calendar.TUESDAY -> (lockedDays and LOCK_TUESDAY) == LOCK_TUESDAY
|
||||
Calendar.WEDNESDAY -> (lockedDays and LOCK_WEDNESDAY) == LOCK_WEDNESDAY
|
||||
Calendar.THURSDAY -> (lockedDays and LOCK_THURSDAY) == LOCK_THURSDAY
|
||||
Calendar.FRIDAY -> (lockedDays and LOCK_FRIDAY) == LOCK_FRIDAY
|
||||
Calendar.SATURDAY -> (lockedDays and LOCK_SATURDAY) == LOCK_SATURDAY
|
||||
else -> false
|
||||
}
|
||||
|
||||
return canLockNow && canLockToday
|
||||
}
|
||||
@@ -99,11 +100,13 @@ interface SecureActivityDelegate {
|
||||
|
||||
// `requireUnlock` can be true on process start or if app was closed in locked state
|
||||
if (!AuthenticatorUtil.isAuthenticating && !requireUnlock) {
|
||||
requireUnlock = /* SY --> */ canLockNow(preferences) && /* SY <-- */ when (val lockDelay = preferences.lockAppAfter().get()) {
|
||||
-1 -> false // Never
|
||||
0 -> true // Always
|
||||
else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis()
|
||||
}
|
||||
requireUnlock =
|
||||
/* SY --> */ canLockNow(preferences) &&
|
||||
/* SY <-- */ when (val lockDelay = preferences.lockAppAfter().get()) {
|
||||
-1 -> false // Never
|
||||
0 -> true // Always
|
||||
else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
lastClosedPref.delete()
|
||||
@@ -140,7 +143,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
||||
val incognitoModeFlow = preferences.incognitoMode().changes()
|
||||
combine(secureScreenFlow, incognitoModeFlow) { secureScreen, incognitoMode ->
|
||||
secureScreen == SecurityPreferences.SecureScreenMode.ALWAYS ||
|
||||
secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode
|
||||
(secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode)
|
||||
}
|
||||
.onEach(activity.window::setSecureScreen)
|
||||
.launchIn(activity.lifecycleScope)
|
||||
|
||||
@@ -41,7 +41,7 @@ class ExtensionsScreenModel(
|
||||
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
||||
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
||||
|
||||
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
|
||||
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
|
||||
|
||||
init {
|
||||
val context = Injekt.get<Application>()
|
||||
@@ -62,14 +62,20 @@ class ExtensionsScreenModel(
|
||||
it.name.contains(input, ignoreCase = true) ||
|
||||
it.baseUrl.contains(input, ignoreCase = true) ||
|
||||
it.id == input.toLongOrNull()
|
||||
} || extension.name.contains(input, ignoreCase = true)
|
||||
} ||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
@@ -80,7 +86,7 @@ class ExtensionsScreenModel(
|
||||
screenModelScope.launchIO {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
||||
_currentDownloads,
|
||||
currentDownloads,
|
||||
getExtensions.subscribe(),
|
||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
val searchQuery = query ?: ""
|
||||
@@ -103,7 +109,8 @@ class ExtensionsScreenModel(
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(LocaleHelper.comparator)
|
||||
.map { (lang, exts) ->
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to exts.map(extensionMapper(downloads))
|
||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||
exts.map(extensionMapper(downloads))
|
||||
}
|
||||
if (languagesWithExtensions.isNotEmpty()) {
|
||||
itemsGroups.putAll(languagesWithExtensions)
|
||||
@@ -165,11 +172,11 @@ class ExtensionsScreenModel(
|
||||
}
|
||||
|
||||
private fun addDownloadState(extension: Extension, installStep: InstallStep) {
|
||||
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||
currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||
}
|
||||
|
||||
private fun removeDownloadState(extension: Extension) {
|
||||
_currentDownloads.update { it - extension.pkgName }
|
||||
currentDownloads.update { it - extension.pkgName }
|
||||
}
|
||||
|
||||
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: Extension) =
|
||||
|
||||
+5
-1
@@ -121,7 +121,11 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
||||
)
|
||||
|
||||
val onDismissRequest = { screenModel.dialog.value = null }
|
||||
when (@Suppress("NAME_SHADOWING") val dialog = dialog) {
|
||||
when
|
||||
(
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val dialog = dialog
|
||||
) {
|
||||
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
||||
MigrationMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
||||
+11
-4
@@ -223,7 +223,9 @@ class MigrationListScreenModel(
|
||||
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
|
||||
}
|
||||
|
||||
if (searchResult != null && !(searchResult.url == mangaObj.url && source.id == mangaObj.source)) {
|
||||
if (searchResult != null &&
|
||||
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
|
||||
) {
|
||||
val localManga = networkToLocalManga.await(searchResult)
|
||||
|
||||
val chapters = if (source is EHentai) {
|
||||
@@ -237,7 +239,8 @@ class MigrationListScreenModel(
|
||||
} catch (e: Exception) {
|
||||
return@async2 null
|
||||
}
|
||||
manga.progress.value = validSources.size to processedSources.incrementAndGet()
|
||||
manga.progress.value =
|
||||
validSources.size to processedSources.incrementAndGet()
|
||||
localManga to chapters.size
|
||||
} else {
|
||||
null
|
||||
@@ -314,7 +317,8 @@ class MigrationListScreenModel(
|
||||
if (result == null && hideNotFound) {
|
||||
removeManga(manga)
|
||||
}
|
||||
if (result != null && showOnlyUpdates &&
|
||||
if (result != null &&
|
||||
showOnlyUpdates &&
|
||||
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
||||
) {
|
||||
removeManga(manga)
|
||||
@@ -363,7 +367,10 @@ class MigrationListScreenModel(
|
||||
|
||||
dbChapters.forEach { chapter ->
|
||||
if (chapter.isRecognizedNumber) {
|
||||
val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber }
|
||||
val prevChapter = prevMangaChapters.find {
|
||||
it.isRecognizedNumber &&
|
||||
it.chapterNumber == chapter.chapterNumber
|
||||
}
|
||||
if (prevChapter != null) {
|
||||
chapterUpdates += ChapterUpdate(
|
||||
id = chapter.id,
|
||||
|
||||
@@ -119,7 +119,10 @@ class SourcesScreenModel(
|
||||
items = byLang
|
||||
.flatMap {
|
||||
listOf(
|
||||
SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null),
|
||||
SourceUiModel.Header(
|
||||
it.key.removePrefix(CATEGORY_KEY_PREFIX),
|
||||
it.value.firstOrNull()?.category != null,
|
||||
),
|
||||
*it.value.map { source ->
|
||||
SourceUiModel.Item(source)
|
||||
}.toTypedArray(),
|
||||
|
||||
@@ -48,7 +48,15 @@ class BiometricTimesScreen : Screen() {
|
||||
fun showTimePicker(startTime: Duration? = null) {
|
||||
val activity = context as? MainActivity ?: return
|
||||
val picker = MaterialTimePicker.Builder()
|
||||
.setTitleText(if (startTime == null) SYMR.strings.biometric_lock_start_time.getString(context) else SYMR.strings.biometric_lock_end_time.getString(context))
|
||||
.setTitleText(
|
||||
if (startTime ==
|
||||
null
|
||||
) {
|
||||
SYMR.strings.biometric_lock_start_time.getString(context)
|
||||
} else {
|
||||
SYMR.strings.biometric_lock_end_time.getString(context)
|
||||
},
|
||||
)
|
||||
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user