Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec14cd9f0 | |||
| c23c9491fc | |||
| 29f3766c87 | |||
| 07c89890bc | |||
| 64a54f55b3 | |||
| f7202e67cc | |||
| 155b03c176 | |||
| 6b0482576b | |||
| c137bafd68 | |||
| 49bdffdc28 | |||
| f1b32d531a | |||
| a5ec6c5cdd | |||
| 9c56cdb1c1 | |||
| 543de065a6 | |||
| 33296e1faf | |||
| d1a90c0bb7 | |||
| 9fa61d33be | |||
| 0e9dcc7855 | |||
| 6738c6072d | |||
| 29033c539c | |||
| eaa3413c37 | |||
| 73d9d1d46d | |||
| 94f9aaf351 | |||
| e21149cb37 | |||
| 11aad16f59 | |||
| 33a3918e86 | |||
| d655b8ecdf | |||
| 70a8bef7a5 | |||
| 999a8613cf | |||
| 5721a02bca | |||
| e303b88b90 | |||
| a62dd5821a | |||
| a0786d9b09 | |||
| 04580ce357 | |||
| b759f2f02a | |||
| 8ae8068ecd | |||
| eecd9367d4 | |||
| 55dee69838 | |||
| a730ca5444 | |||
| cebd8fe0a8 | |||
| 55a979c5f7 | |||
| 728f3fc349 | |||
| a9a3ed1d16 | |||
| 36f13a7c6a | |||
| 37a2ccc678 | |||
| bb39088dd7 | |||
| c5546e1095 | |||
| 2d12c670db | |||
| 3db4bccebc | |||
| 2f23ad6bfd | |||
| de1898a2c9 | |||
| eb135ec22d | |||
| bf6c646dc7 | |||
| 9ce16d5e1c | |||
| 619ff726c8 | |||
| 730ceaaf49 | |||
| 07b701cb3c | |||
| b64c6b78ea | |||
| 521bce5c08 | |||
| a719ed8c9e | |||
| f6fc2d7e2f | |||
| 48d43c4f07 | |||
| f4fa86b2dc | |||
| 37db0dc1f6 | |||
| 1ada03b07a | |||
| f4c1e7c2d5 | |||
| 6c5282c598 | |||
| 7899474a36 | |||
| 225b419bba | |||
| fa64103a1c | |||
| 57e0e99f06 | |||
| efde7afa8e | |||
| f929a4bc26 | |||
| d35141c1cc | |||
| 6988966019 | |||
| f6d8ebbb0a | |||
| ae45df9fcf | |||
| f332344681 | |||
| c6abb340ca | |||
| 99dbb16a7a | |||
| f62e8933d7 | |||
| ee3c2fd79c | |||
| 6b08b873a8 | |||
| a3f2f49ab8 | |||
| 524f5cc6ab | |||
| a35e084b9e | |||
| 78f7fba67b | |||
| 69d1db3018 | |||
| d1b317e5c8 | |||
| fff40e031f | |||
| 5be2ec51ba | |||
| 1c2a7af13e | |||
| 182158acb0 | |||
| 21f92bfb3a | |||
| a5522ef732 | |||
| 239793f7fd | |||
| 4e9cfe4602 | |||
| f548c85e7a | |||
| 576349c446 | |||
| 9b00e0458b | |||
| 6a1ff99441 | |||
| 0121fe9397 | |||
| 5c47c7a409 | |||
| 8bb4f33f2e | |||
| 5f5fd51668 | |||
| c7bbad93b2 | |||
| 1a4a2506f4 | |||
| 7b7a594ddb | |||
| c2eece0fff | |||
| d29a4ff381 |
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
],
|
||||||
|
"labels": ["Dependencies"],
|
||||||
|
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v4
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
@@ -27,19 +27,19 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: adopt
|
distribution: adopt
|
||||||
|
|
||||||
- name: Set up gradle
|
- name: Set up gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: ./gradlew detekt assembleDevDebug
|
run: ./gradlew detekt assembleDevDebug
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: TachiyomiSY-${{ github.sha }}.apk
|
name: TachiyomiSY-${{ github.sha }}.apk
|
||||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v2
|
uses: gradle/actions/wrapper-validation@v4
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
run: |
|
run: |
|
||||||
@@ -31,18 +31,18 @@ jobs:
|
|||||||
distribution: adopt
|
distribution: adopt
|
||||||
|
|
||||||
- name: Set up gradle
|
- name: Set up gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
# SY <--
|
# SY <--
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
uses: DamianReeves/write-file-action@v1.2
|
uses: DamianReeves/write-file-action@v1.3
|
||||||
with:
|
with:
|
||||||
path: app/google-services.json
|
path: app/google-services.json
|
||||||
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
||||||
write-mode: overwrite
|
write-mode: overwrite
|
||||||
|
|
||||||
- name: Write client_secrets.json
|
- name: Write client_secrets.json
|
||||||
uses: DamianReeves/write-file-action@v1.2
|
uses: DamianReeves/write-file-action@v1.3
|
||||||
with:
|
with:
|
||||||
path: app/src/main/assets/client_secrets.json
|
path: app/src/main/assets/client_secrets.json
|
||||||
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
|
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.run_number }}
|
tag_name: ${{ github.run_number }}
|
||||||
name: TachiyomiSY
|
name: TachiyomiSY
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ name: Remote Dispatch Action Initiator
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'preview'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger_preview_build:
|
trigger_preview_build:
|
||||||
name: Trigger preview build
|
name: Trigger preview build
|
||||||
if: ${{ github.ref == 'refs/heads/master' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -16,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v4
|
||||||
|
|
||||||
- name: Create Tag
|
- name: Create Tag
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.gradle
|
.gradle
|
||||||
|
.kotlin
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ Before you start, please note that the ability to use following technologies is
|
|||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
|
|
||||||
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
|
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
|
## Getting help
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Features of Mihon(original) include:
|
|||||||
* Online reading from a variety of sources
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded content
|
* Local reading of downloaded content
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@@ -67,6 +67,15 @@ Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/re
|
|||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/mihon/tachiyomisy/)
|
||||||
|
|
||||||
|
<details><summary>Translation Progress</summary>
|
||||||
|
<a href="https://hosted.weblate.org/engage/mihon/">
|
||||||
|
<img src="https://hosted.weblate.org/widgets/mihon/-/tachiyomisy/multi-auto.svg" alt="Translation status" />
|
||||||
|
</a>
|
||||||
|
</details>
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
|
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
|
|
||||||
versionCode = 68
|
versionCode = 69
|
||||||
versionName = "1.10.5"
|
versionName = "1.10.5"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
@@ -162,13 +162,15 @@ dependencies {
|
|||||||
implementation(compose.ui.tooling.preview)
|
implementation(compose.ui.tooling.preview)
|
||||||
implementation(compose.ui.util)
|
implementation(compose.ui.util)
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
implementation(compose.accompanist.systemuicontroller)
|
||||||
|
|
||||||
|
implementation(androidx.interpolator)
|
||||||
|
|
||||||
implementation(androidx.paging.runtime)
|
implementation(androidx.paging.runtime)
|
||||||
implementation(androidx.paging.compose)
|
implementation(androidx.paging.compose)
|
||||||
|
|
||||||
implementation(libs.bundles.sqlite)
|
implementation(libs.bundles.sqlite)
|
||||||
// SY -->
|
// SY -->
|
||||||
implementation(libs.sqlcipher)
|
implementation(sylibs.sqlcipher)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
implementation(kotlinx.reflect)
|
implementation(kotlinx.reflect)
|
||||||
@@ -246,9 +248,6 @@ dependencies {
|
|||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
|
|
||||||
|
|
||||||
implementation(libs.google.api.services.drive)
|
|
||||||
implementation(libs.google.api.client.oauth)
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
@@ -281,6 +280,10 @@ dependencies {
|
|||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation(sylibs.ratingbar)
|
implementation(sylibs.ratingbar)
|
||||||
implementation(sylibs.composeRatingbar)
|
implementation(sylibs.composeRatingbar)
|
||||||
|
|
||||||
|
// Google drive
|
||||||
|
implementation(sylibs.google.api.services.drive)
|
||||||
|
implementation(sylibs.google.api.client.oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
@@ -302,7 +305,7 @@ androidComponents {
|
|||||||
tasks {
|
tasks {
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
compilerOptions.freeCompilerArgs.addAll(
|
||||||
"-Xcontext-receivers",
|
"-Xcontext-receivers",
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ class SourcePreferences(
|
|||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun globalSearchFilterState() = preferenceStore.getBoolean(
|
||||||
|
Preference.appStateKey("has_filters_toggle_state"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)
|
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class SyncPreferences(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun uniqueDeviceID(): String {
|
fun uniqueDeviceID(): String {
|
||||||
val uniqueIDPreference = preferenceStore.getString("unique_device_id", "")
|
val uniqueIDPreference = preferenceStore.getString(Preference.appStateKey("unique_device_id"), "")
|
||||||
|
|
||||||
// Retrieve the current value of the preference
|
// Retrieve the current value of the preference
|
||||||
var uniqueID = uniqueIDPreference.get()
|
var uniqueID = uniqueIDPreference.get()
|
||||||
@@ -53,12 +53,14 @@ class SyncPreferences(
|
|||||||
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
||||||
history = preferenceStore.getBoolean("history", true).get(),
|
history = preferenceStore.getBoolean("history", true).get(),
|
||||||
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||||
|
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
|
||||||
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||||
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
||||||
readEntries = preferenceStore.getBoolean("readEntries", true).get()
|
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
|
||||||
|
savedSearches = preferenceStore.getBoolean("savedSearches", true).get(),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -70,12 +72,14 @@ class SyncPreferences(
|
|||||||
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
||||||
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
||||||
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||||
|
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
|
||||||
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||||
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
||||||
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
||||||
|
preferenceStore.getBoolean("savedSearches", true).set(syncSettings.savedSearches)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ data class SyncSettings(
|
|||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val customInfo: Boolean = true,
|
val customInfo: Boolean = true,
|
||||||
val readEntries: Boolean = true
|
val readEntries: Boolean = true,
|
||||||
|
val savedSearches: Boolean = true,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ private fun ColumnScope.FilterPage(
|
|||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val trackers = remember { screenModel.trackers }
|
val trackers by screenModel.trackersFlow.collectAsState()
|
||||||
when (trackers.size) {
|
when (trackers.size) {
|
||||||
0 -> {
|
0 -> {
|
||||||
// No trackers
|
// No trackers
|
||||||
@@ -188,6 +188,7 @@ private fun ColumnScope.SortPage(
|
|||||||
category: Category?,
|
category: Category?,
|
||||||
screenModel: LibrarySettingsScreenModel,
|
screenModel: LibrarySettingsScreenModel,
|
||||||
) {
|
) {
|
||||||
|
val trackers by screenModel.trackersFlow.collectAsState()
|
||||||
// SY -->
|
// SY -->
|
||||||
val globalSortMode by screenModel.libraryPreferences.sortingMode().collectAsState()
|
val globalSortMode by screenModel.libraryPreferences.sortingMode().collectAsState()
|
||||||
val sortingMode = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
|
val sortingMode = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
|
||||||
@@ -206,12 +207,12 @@ private fun ColumnScope.SortPage(
|
|||||||
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
|
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val trackerSortOption =
|
|
||||||
if (screenModel.trackers.isEmpty()) {
|
val trackerSortOption = if (trackers.isEmpty()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
|
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
|
||||||
}
|
}
|
||||||
|
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
|
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
|
||||||
@@ -346,12 +347,13 @@ private fun ColumnScope.GroupPage(
|
|||||||
screenModel: LibrarySettingsScreenModel,
|
screenModel: LibrarySettingsScreenModel,
|
||||||
hasCategories: Boolean,
|
hasCategories: Boolean,
|
||||||
) {
|
) {
|
||||||
val groups = remember(hasCategories, screenModel.trackers) {
|
val trackers by screenModel.trackersFlow.collectAsState()
|
||||||
|
val groups = remember(hasCategories, trackers) {
|
||||||
buildList {
|
buildList {
|
||||||
add(LibraryGroup.BY_DEFAULT)
|
add(LibraryGroup.BY_DEFAULT)
|
||||||
add(LibraryGroup.BY_SOURCE)
|
add(LibraryGroup.BY_SOURCE)
|
||||||
add(LibraryGroup.BY_STATUS)
|
add(LibraryGroup.BY_STATUS)
|
||||||
if (screenModel.trackers.isNotEmpty()) {
|
if (trackers.isNotEmpty()) {
|
||||||
add(LibraryGroup.BY_TRACK_STATUS)
|
add(LibraryGroup.BY_TRACK_STATUS)
|
||||||
}
|
}
|
||||||
if (hasCategories) {
|
if (hasCategories) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.Shadow
|
import androidx.compose.ui.graphics.Shadow
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
@@ -42,15 +43,22 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.presentation.core.components.BadgeGroup
|
import tachiyomi.presentation.core.components.BadgeGroup
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.selectedBackground
|
import tachiyomi.presentation.core.util.selectedBackground
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover as MangaCoverModel
|
||||||
|
|
||||||
object CommonMangaItemDefaults {
|
object CommonMangaItemDefaults {
|
||||||
val GridHorizontalSpacer = 4.dp
|
val GridHorizontalSpacer = 4.dp
|
||||||
val GridVerticalSpacer = 4.dp
|
val GridVerticalSpacer = 4.dp
|
||||||
|
|
||||||
|
@Suppress("ConstPropertyName")
|
||||||
const val BrowseFavoriteCoverAlpha = 0.34f
|
const val BrowseFavoriteCoverAlpha = 0.34f
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ContinueReadingButtonSize = 28.dp
|
private val ContinueReadingButtonSizeSmall = 28.dp
|
||||||
|
private val ContinueReadingButtonSizeLarge = 32.dp
|
||||||
|
|
||||||
|
private val ContinueReadingButtonIconSizeSmall = 16.dp
|
||||||
|
private val ContinueReadingButtonIconSizeLarge = 20.dp
|
||||||
|
|
||||||
private val ContinueReadingButtonGridPadding = 6.dp
|
private val ContinueReadingButtonGridPadding = 6.dp
|
||||||
private val ContinueReadingButtonListSpacing = 8.dp
|
private val ContinueReadingButtonListSpacing = 8.dp
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ private const val GridSelectedCoverAlpha = 0.76f
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCompactGridItem(
|
fun MangaCompactGridItem(
|
||||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
coverData: MangaCoverModel,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
@@ -96,10 +104,12 @@ fun MangaCompactGridItem(
|
|||||||
)
|
)
|
||||||
} else if (onClickContinueReading != null) {
|
} else if (onClickContinueReading != null) {
|
||||||
ContinueReadingButton(
|
ContinueReadingButton(
|
||||||
|
size = ContinueReadingButtonSizeLarge,
|
||||||
|
iconSize = ContinueReadingButtonIconSizeLarge,
|
||||||
|
onClick = onClickContinueReading,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(ContinueReadingButtonGridPadding)
|
.padding(ContinueReadingButtonGridPadding)
|
||||||
.align(Alignment.BottomEnd),
|
.align(Alignment.BottomEnd),
|
||||||
onClickContinueReading = onClickContinueReading,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -148,11 +158,13 @@ private fun BoxScope.CoverTextOverlay(
|
|||||||
)
|
)
|
||||||
if (onClickContinueReading != null) {
|
if (onClickContinueReading != null) {
|
||||||
ContinueReadingButton(
|
ContinueReadingButton(
|
||||||
|
size = ContinueReadingButtonSizeSmall,
|
||||||
|
iconSize = ContinueReadingButtonIconSizeSmall,
|
||||||
|
onClick = onClickContinueReading,
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
end = ContinueReadingButtonGridPadding,
|
end = ContinueReadingButtonGridPadding,
|
||||||
bottom = ContinueReadingButtonGridPadding,
|
bottom = ContinueReadingButtonGridPadding,
|
||||||
),
|
),
|
||||||
onClickContinueReading = onClickContinueReading,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +175,7 @@ private fun BoxScope.CoverTextOverlay(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaComfortableGridItem(
|
fun MangaComfortableGridItem(
|
||||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
coverData: MangaCoverModel,
|
||||||
title: String,
|
title: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
@@ -194,10 +206,12 @@ fun MangaComfortableGridItem(
|
|||||||
content = {
|
content = {
|
||||||
if (onClickContinueReading != null) {
|
if (onClickContinueReading != null) {
|
||||||
ContinueReadingButton(
|
ContinueReadingButton(
|
||||||
|
size = ContinueReadingButtonSizeLarge,
|
||||||
|
iconSize = ContinueReadingButtonIconSizeLarge,
|
||||||
|
onClick = onClickContinueReading,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(ContinueReadingButtonGridPadding)
|
.padding(ContinueReadingButtonGridPadding)
|
||||||
.align(Alignment.BottomEnd),
|
.align(Alignment.BottomEnd),
|
||||||
onClickContinueReading = onClickContinueReading,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -309,14 +323,14 @@ private fun GridItemSelectable(
|
|||||||
private fun Modifier.selectedOutline(
|
private fun Modifier.selectedOutline(
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
color: Color,
|
color: Color,
|
||||||
) = this then drawBehind { if (isSelected) drawRect(color = color) }
|
) = drawBehind { if (isSelected) drawRect(color = color) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout of list item.
|
* Layout of list item.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaListItem(
|
fun MangaListItem(
|
||||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
coverData: MangaCoverModel,
|
||||||
title: String,
|
title: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
@@ -354,8 +368,10 @@ fun MangaListItem(
|
|||||||
BadgeGroup(content = badge)
|
BadgeGroup(content = badge)
|
||||||
if (onClickContinueReading != null) {
|
if (onClickContinueReading != null) {
|
||||||
ContinueReadingButton(
|
ContinueReadingButton(
|
||||||
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
|
size = ContinueReadingButtonSizeSmall,
|
||||||
onClickContinueReading = onClickContinueReading,
|
iconSize = ContinueReadingButtonIconSizeSmall,
|
||||||
|
onClick = onClickContinueReading,
|
||||||
|
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,23 +379,25 @@ fun MangaListItem(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContinueReadingButton(
|
private fun ContinueReadingButton(
|
||||||
|
size: Dp,
|
||||||
|
iconSize: Dp,
|
||||||
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickContinueReading: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
onClick = onClickContinueReading,
|
onClick = onClick,
|
||||||
modifier = Modifier.size(ContinueReadingButtonSize),
|
|
||||||
shape = MaterialTheme.shapes.small,
|
shape = MaterialTheme.shapes.small,
|
||||||
colors = IconButtonDefaults.filledIconButtonColors(
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||||
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
|
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
|
||||||
),
|
),
|
||||||
|
modifier = Modifier.size(size)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
contentDescription = stringResource(MR.strings.action_resume),
|
contentDescription = stringResource(MR.strings.action_resume),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(iconSize),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -69,9 +68,6 @@ fun MangaChapterListItem(
|
|||||||
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val textAlpha = if (read) ReadItemAlpha else 1f
|
|
||||||
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
|
|
||||||
|
|
||||||
val start = getSwipeAction(
|
val start = getSwipeAction(
|
||||||
action = chapterSwipeStartAction,
|
action = chapterSwipeStartAction,
|
||||||
read = read,
|
read = read,
|
||||||
@@ -136,15 +132,20 @@ fun MangaChapterListItem(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = LocalContentColor.current.copy(alpha = textAlpha),
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
onTextLayout = { textHeight = it.size.height },
|
onTextLayout = { textHeight = it.size.height },
|
||||||
|
color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
|
Row {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
val subtitleStyle = MaterialTheme.typography.bodySmall
|
||||||
|
.merge(
|
||||||
|
color = LocalContentColor.current
|
||||||
|
.copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha)
|
||||||
|
)
|
||||||
|
ProvideTextStyle(value = subtitleStyle) {
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
Text(
|
Text(
|
||||||
text = date,
|
text = date,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
|
|||||||
@@ -102,9 +102,12 @@ fun SetIntervalDialog(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
Text(
|
||||||
|
stringResource(MR.strings.manga_interval_expected_update_null),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||||
|
|
||||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.presentation.manga.components
|
|||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||||
@@ -289,7 +290,8 @@ fun ExpandableMangaDescription(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(vertical = 12.dp)
|
.padding(vertical = 12.dp)
|
||||||
.animateContentSize(),
|
.animateContentSize(animationSpec = spring())
|
||||||
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
var tagSelected by remember { mutableStateOf("") }
|
var tagSelected by remember { mutableStateOf("") }
|
||||||
|
|||||||
@@ -111,8 +111,14 @@ fun ScanlatorFilterDialog(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
FlowRow {
|
FlowRow {
|
||||||
TextButton(onClick = mutableExcludedScanlators::clear) {
|
if (mutableExcludedScanlators.isEmpty()) {
|
||||||
Text(text = stringResource(MR.strings.action_reset))
|
TextButton(onClick = { mutableExcludedScanlators.addAll(availableScanlators) }) {
|
||||||
|
Text(text = stringResource(MR.strings.action_select_all))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextButton(onClick = mutableExcludedScanlators::clear) {
|
||||||
|
Text(text = stringResource(MR.strings.action_reset))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.structuralEqualityPolicy
|
import androidx.compose.runtime.structuralEqualityPolicy
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
|
||||||
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.InfoWidget
|
import eu.kanade.presentation.more.settings.widget.InfoWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
|
||||||
@@ -23,8 +23,6 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.presentation.core.components.SliderItem
|
import tachiyomi.presentation.core.components.SliderItem
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
|
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||||
val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp }
|
val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp }
|
||||||
@@ -156,16 +154,14 @@ internal fun PreferenceItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.TrackerPreference -> {
|
is Preference.PreferenceItem.TrackerPreference -> {
|
||||||
val uName by Injekt.get<TrackPreferences>()
|
val isLoggedIn by item.tracker.let { tracker ->
|
||||||
.trackUsername(item.tracker)
|
tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn)
|
||||||
.collectAsState()
|
|
||||||
item.tracker.run {
|
|
||||||
TrackingPreferenceWidget(
|
|
||||||
tracker = this,
|
|
||||||
checked = uName.isNotEmpty(),
|
|
||||||
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
TrackingPreferenceWidget(
|
||||||
|
tracker = item.tracker,
|
||||||
|
checked = isLoggedIn,
|
||||||
|
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.InfoPreference -> {
|
is Preference.PreferenceItem.InfoPreference -> {
|
||||||
InfoWidget(text = item.title)
|
InfoWidget(text = item.title)
|
||||||
|
|||||||
+11
-1
@@ -127,7 +127,17 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
// For some reason InkBook devices do not implement the SAF properly. Persistable URI grants do not
|
||||||
|
// work. However, simply retrieving the URI and using it works fine for these devices. Access is not
|
||||||
|
// revoked after the app is closed or the device is restarted.
|
||||||
|
// This also holds for some Samsung devices. Thus, we simply execute inside of a try-catch block and
|
||||||
|
// ignore the exception if it is thrown.
|
||||||
|
try {
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
context.toast(MR.strings.file_picker_uri_permission_unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
UniFile.fromUri(context, uri)?.let {
|
UniFile.fromUri(context, uri)?.let {
|
||||||
storageDirPref.set(it.uri.toString())
|
storageDirPref.set(it.uri.toString())
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||||
|
|
||||||
val defaultCategory by libraryPreferences.defaultCategory().collectAsState()
|
|
||||||
val selectedCategory = allCategories.find { it.id == defaultCategory.toLong() }
|
|
||||||
|
|
||||||
// For default category
|
// For default category
|
||||||
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
||||||
allCategories.fastMap { it.id.toInt() }
|
allCategories.fastMap { it.id.toInt() }
|
||||||
@@ -108,7 +105,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.defaultCategory(),
|
pref = libraryPreferences.defaultCategory(),
|
||||||
title = stringResource(MR.strings.default_category),
|
title = stringResource(MR.strings.default_category),
|
||||||
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
|
||||||
entries = ids.zip(labels).toMap().toImmutableMap(),
|
entries = ids.zip(labels).toMap().toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
|||||||
+61
-5
@@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentMapOf
|
|||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -88,12 +89,8 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_page_transitions),
|
title = stringResource(MR.strings.pref_page_transitions),
|
||||||
),
|
),
|
||||||
SY <-- */
|
SY <-- */
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = readerPref.flashOnPageChange(),
|
|
||||||
title = stringResource(MR.strings.pref_flash_page),
|
|
||||||
subtitle = stringResource(MR.strings.pref_flash_page_summ),
|
|
||||||
),
|
|
||||||
getDisplayGroup(readerPreferences = readerPref),
|
getDisplayGroup(readerPreferences = readerPref),
|
||||||
|
getEInkGroup(readerPreferences = readerPref),
|
||||||
getReadingGroup(readerPreferences = readerPref),
|
getReadingGroup(readerPreferences = readerPref),
|
||||||
getPagedGroup(readerPreferences = readerPref),
|
getPagedGroup(readerPreferences = readerPref),
|
||||||
getWebtoonGroup(readerPreferences = readerPref),
|
getWebtoonGroup(readerPreferences = readerPref),
|
||||||
@@ -156,6 +153,65 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getEInkGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
|
val flashPageState by readerPreferences.flashOnPageChange().collectAsState()
|
||||||
|
|
||||||
|
val flashMillisPref = readerPreferences.flashDurationMillis()
|
||||||
|
val flashMillis by flashMillisPref.collectAsState()
|
||||||
|
|
||||||
|
val flashIntervalPref = readerPreferences.flashPageInterval()
|
||||||
|
val flashInterval by flashIntervalPref.collectAsState()
|
||||||
|
|
||||||
|
val flashColorPref = readerPreferences.flashColor()
|
||||||
|
|
||||||
|
return Preference.PreferenceGroup(
|
||||||
|
title = "E-Ink",
|
||||||
|
preferenceItems = persistentListOf(
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = readerPreferences.flashOnPageChange(),
|
||||||
|
title = stringResource(MR.strings.pref_flash_page),
|
||||||
|
subtitle = stringResource(MR.strings.pref_flash_page_summ),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
|
min = 1,
|
||||||
|
max = 15,
|
||||||
|
title = stringResource(MR.strings.pref_flash_duration),
|
||||||
|
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
|
onValueChanged = {
|
||||||
|
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
||||||
|
true
|
||||||
|
},
|
||||||
|
enabled = flashPageState,
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = flashInterval,
|
||||||
|
min = 1,
|
||||||
|
max = 10,
|
||||||
|
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||||
|
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
|
onValueChanged = {
|
||||||
|
flashIntervalPref.set(it)
|
||||||
|
true
|
||||||
|
},
|
||||||
|
enabled = flashPageState,
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = flashColorPref,
|
||||||
|
title = stringResource(MR.strings.pref_flash_with),
|
||||||
|
entries = persistentMapOf(
|
||||||
|
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
|
||||||
|
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
|
||||||
|
ReaderPreferences.FlashColor.WHITE_BLACK
|
||||||
|
to stringResource(MR.strings.pref_flash_style_white_black),
|
||||||
|
),
|
||||||
|
enabled = flashPageState,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
|
|||||||
+2
-3
@@ -6,7 +6,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -69,7 +68,7 @@ class CreateBackupScreen : Screen() {
|
|||||||
LazyColumnWithAction(
|
LazyColumnWithAction(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
actionLabel = stringResource(MR.strings.action_create),
|
actionLabel = stringResource(MR.strings.action_create),
|
||||||
actionEnabled = state.options.anyEnabled(),
|
actionEnabled = state.options.canCreate(),
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +103,7 @@ class CreateBackupScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.Options(
|
private fun Options(
|
||||||
options: ImmutableList<BackupOptions.Entry>,
|
options: ImmutableList<BackupOptions.Entry>,
|
||||||
state: CreateBackupScreenModel.State,
|
state: CreateBackupScreenModel.State,
|
||||||
model: CreateBackupScreenModel,
|
model: CreateBackupScreenModel,
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ class RestoreBackupScreen(
|
|||||||
LazyColumnWithAction(
|
LazyColumnWithAction(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
actionLabel = stringResource(MR.strings.action_restore),
|
actionLabel = stringResource(MR.strings.action_restore),
|
||||||
actionEnabled = state.canRestore && state.options.anyEnabled(),
|
actionEnabled = state.canRestore && state.options.canRestore(),
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
model.startRestore()
|
model.startRestore()
|
||||||
navigator.pop()
|
navigator.pop()
|
||||||
|
|||||||
+5
-1
@@ -49,7 +49,7 @@ class SyncSettingsSelector : Screen() {
|
|||||||
LazyColumnWithAction(
|
LazyColumnWithAction(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
actionLabel = stringResource(SYMR.strings.label_sync),
|
actionLabel = stringResource(SYMR.strings.label_sync),
|
||||||
actionEnabled = state.options.anyEnabled(),
|
actionEnabled = state.options.canCreate(),
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
if (!SyncDataJob.isRunning(context)) {
|
if (!SyncDataJob.isRunning(context)) {
|
||||||
model.syncNow(context)
|
model.syncNow(context)
|
||||||
@@ -122,12 +122,14 @@ private class SyncSettingsSelectorModel(
|
|||||||
tracking = syncSettings.tracking,
|
tracking = syncSettings.tracking,
|
||||||
history = syncSettings.history,
|
history = syncSettings.history,
|
||||||
appSettings = syncSettings.appSettings,
|
appSettings = syncSettings.appSettings,
|
||||||
|
extensionRepoSettings = syncSettings.extensionRepoSettings,
|
||||||
sourceSettings = syncSettings.sourceSettings,
|
sourceSettings = syncSettings.sourceSettings,
|
||||||
privateSettings = syncSettings.privateSettings,
|
privateSettings = syncSettings.privateSettings,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = syncSettings.customInfo,
|
customInfo = syncSettings.customInfo,
|
||||||
readEntries = syncSettings.readEntries,
|
readEntries = syncSettings.readEntries,
|
||||||
|
savedSearches = syncSettings.savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -140,12 +142,14 @@ private class SyncSettingsSelectorModel(
|
|||||||
tracking = backupOptions.tracking,
|
tracking = backupOptions.tracking,
|
||||||
history = backupOptions.history,
|
history = backupOptions.history,
|
||||||
appSettings = backupOptions.appSettings,
|
appSettings = backupOptions.appSettings,
|
||||||
|
extensionRepoSettings = backupOptions.extensionRepoSettings,
|
||||||
sourceSettings = backupOptions.sourceSettings,
|
sourceSettings = backupOptions.sourceSettings,
|
||||||
privateSettings = backupOptions.privateSettings,
|
privateSettings = backupOptions.privateSettings,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = backupOptions.customInfo,
|
customInfo = backupOptions.customInfo,
|
||||||
readEntries = backupOptions.readEntries,
|
readEntries = backupOptions.readEntries,
|
||||||
|
savedSearches = backupOptions.savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,42 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class DisplayRefreshHost {
|
class DisplayRefreshHost {
|
||||||
|
|
||||||
internal var currentDisplayRefresh by mutableStateOf(false)
|
internal var currentDisplayRefresh by mutableStateOf(false)
|
||||||
|
private val readerPreferences = Injekt.get<ReaderPreferences>()
|
||||||
|
|
||||||
|
internal val flashMillis = readerPreferences.flashDurationMillis()
|
||||||
|
internal val flashMode = readerPreferences.flashColor()
|
||||||
|
|
||||||
|
internal val flashIntervalPref = readerPreferences.flashPageInterval()
|
||||||
|
|
||||||
|
// Internal State for Flash
|
||||||
|
private var flashInterval = flashIntervalPref.get()
|
||||||
|
private var timesCalled = 0
|
||||||
|
|
||||||
fun flash() {
|
fun flash() {
|
||||||
currentDisplayRefresh = true
|
if (timesCalled % flashInterval == 0) {
|
||||||
|
currentDisplayRefresh = true
|
||||||
|
}
|
||||||
|
timesCalled += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInterval(interval: Int) {
|
||||||
|
flashInterval = interval
|
||||||
|
timesCalled = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,18 +52,39 @@ fun DisplayRefreshHost(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val currentDisplayRefresh = hostState.currentDisplayRefresh
|
val currentDisplayRefresh = hostState.currentDisplayRefresh
|
||||||
|
val refreshDuration by hostState.flashMillis.collectAsState()
|
||||||
|
val flashMode by hostState.flashMode.collectAsState()
|
||||||
|
val flashInterval by hostState.flashIntervalPref.collectAsState()
|
||||||
|
|
||||||
|
var currentColor by remember { mutableStateOf<Color?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(currentDisplayRefresh) {
|
LaunchedEffect(currentDisplayRefresh) {
|
||||||
if (currentDisplayRefresh) {
|
if (!currentDisplayRefresh) {
|
||||||
delay(1.5.seconds)
|
currentColor = null
|
||||||
hostState.currentDisplayRefresh = false
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val refreshDurationHalf = refreshDuration.milliseconds / 2
|
||||||
|
currentColor = if (flashMode == ReaderPreferences.FlashColor.BLACK) {
|
||||||
|
Color.Black
|
||||||
|
} else {
|
||||||
|
Color.White
|
||||||
|
}
|
||||||
|
delay(refreshDurationHalf)
|
||||||
|
if (flashMode == ReaderPreferences.FlashColor.WHITE_BLACK) {
|
||||||
|
currentColor = Color.Black
|
||||||
|
}
|
||||||
|
delay(refreshDurationHalf)
|
||||||
|
hostState.currentDisplayRefresh = false
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(flashInterval) {
|
||||||
|
hostState.setInterval(flashInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
if (currentDisplayRefresh) {
|
currentColor?.let { drawRect(it) }
|
||||||
drawRect(Color.Black)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Photo
|
import androidx.compose.material.icons.outlined.Photo
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
@@ -31,9 +32,9 @@ fun ReaderPageActionsDialog(
|
|||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onSetAsCover: (useExtraPage: Boolean) -> Unit,
|
onSetAsCover: (useExtraPage: Boolean) -> Unit,
|
||||||
onShare: (useExtraPage: Boolean) -> Unit,
|
onShare: (copy: Boolean, useExtraPage: Boolean) -> Unit,
|
||||||
onSave: (useExtraPage: Boolean) -> Unit,
|
onSave: (useExtraPage: Boolean) -> Unit,
|
||||||
onShareCombined: () -> Unit,
|
onShareCombined: (copy: Boolean) -> Unit,
|
||||||
onSaveCombined: () -> Unit,
|
onSaveCombined: () -> Unit,
|
||||||
hasExtraPage: Boolean,
|
hasExtraPage: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -62,6 +63,25 @@ fun ReaderPageActionsDialog(
|
|||||||
icon = Icons.Outlined.Photo,
|
icon = Icons.Outlined.Photo,
|
||||||
onClick = { showSetCoverDialog = true },
|
onClick = { showSetCoverDialog = true },
|
||||||
)
|
)
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
title = stringResource(
|
||||||
|
// SY -->
|
||||||
|
if (hasExtraPage) {
|
||||||
|
SYMR.strings.action_copy_first_page
|
||||||
|
} else {
|
||||||
|
MR.strings.action_copy_to_clipboard
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
),
|
||||||
|
icon = Icons.Outlined.ContentCopy,
|
||||||
|
onClick = {
|
||||||
|
// SY -->
|
||||||
|
onShare(true, false)
|
||||||
|
// SY <--
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
title = stringResource(
|
title = stringResource(
|
||||||
@@ -76,7 +96,7 @@ fun ReaderPageActionsDialog(
|
|||||||
icon = Icons.Outlined.Share,
|
icon = Icons.Outlined.Share,
|
||||||
onClick = {
|
onClick = {
|
||||||
// SY -->
|
// SY -->
|
||||||
onShare(false)
|
onShare(false, false)
|
||||||
// SY <--
|
// SY <--
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
@@ -114,12 +134,21 @@ fun ReaderPageActionsDialog(
|
|||||||
showSetCoverDialog = true
|
showSetCoverDialog = true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
title = stringResource(SYMR.strings.action_copy_second_page),
|
||||||
|
icon = Icons.Outlined.ContentCopy,
|
||||||
|
onClick = {
|
||||||
|
onShare(true, true)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
title = stringResource(SYMR.strings.action_share_second_page),
|
title = stringResource(SYMR.strings.action_share_second_page),
|
||||||
icon = Icons.Outlined.Share,
|
icon = Icons.Outlined.Share,
|
||||||
onClick = {
|
onClick = {
|
||||||
onShare(true)
|
onShare(false, true)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -136,12 +165,21 @@ fun ReaderPageActionsDialog(
|
|||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
title = stringResource(SYMR.strings.action_copy_combined_page),
|
||||||
|
icon = Icons.Outlined.ContentCopy,
|
||||||
|
onClick = {
|
||||||
|
onShareCombined(true)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
title = stringResource(SYMR.strings.action_share_combined_page),
|
title = stringResource(SYMR.strings.action_share_combined_page),
|
||||||
icon = Icons.Outlined.Share,
|
icon = Icons.Outlined.Share,
|
||||||
onClick = {
|
onClick = {
|
||||||
onShareCombined()
|
onShareCombined(false)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import androidx.compose.material3.FilterChip
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||||
|
import tachiyomi.presentation.core.components.SliderItem
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
|
||||||
@@ -20,9 +23,27 @@ private val themes = listOf(
|
|||||||
MR.strings.automatic_background to 3,
|
MR.strings.automatic_background to 3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val flashColors = listOf(
|
||||||
|
MR.strings.pref_flash_style_black to ReaderPreferences.FlashColor.BLACK,
|
||||||
|
MR.strings.pref_flash_style_white to ReaderPreferences.FlashColor.WHITE,
|
||||||
|
MR.strings.pref_flash_style_white_black to ReaderPreferences.FlashColor.WHITE_BLACK,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||||
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
|
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
|
||||||
|
|
||||||
|
val flashPageState by screenModel.preferences.flashOnPageChange().collectAsState()
|
||||||
|
|
||||||
|
val flashMillisPref = screenModel.preferences.flashDurationMillis()
|
||||||
|
val flashMillis by flashMillisPref.collectAsState()
|
||||||
|
|
||||||
|
val flashIntervalPref = screenModel.preferences.flashPageInterval()
|
||||||
|
val flashInterval by flashIntervalPref.collectAsState()
|
||||||
|
|
||||||
|
val flashColorPref = screenModel.preferences.flashColor()
|
||||||
|
val flashColor by flashColorPref.collectAsState()
|
||||||
|
|
||||||
SettingsChipRow(MR.strings.pref_reader_theme) {
|
SettingsChipRow(MR.strings.pref_reader_theme) {
|
||||||
themes.map { (labelRes, value) ->
|
themes.map { (labelRes, value) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
@@ -95,6 +116,35 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
label = stringResource(MR.strings.pref_flash_page),
|
label = stringResource(MR.strings.pref_flash_page),
|
||||||
pref = screenModel.preferences.flashOnPageChange(),
|
pref = screenModel.preferences.flashOnPageChange(),
|
||||||
)
|
)
|
||||||
|
if (flashPageState) {
|
||||||
|
SliderItem(
|
||||||
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
|
label = stringResource(MR.strings.pref_flash_duration),
|
||||||
|
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
|
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||||
|
min = 1,
|
||||||
|
max = 15,
|
||||||
|
)
|
||||||
|
SliderItem(
|
||||||
|
value = flashInterval,
|
||||||
|
label = stringResource(MR.strings.pref_flash_page_interval),
|
||||||
|
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
|
onChange = {
|
||||||
|
flashIntervalPref.set(it)
|
||||||
|
},
|
||||||
|
min = 1,
|
||||||
|
max = 10,
|
||||||
|
)
|
||||||
|
SettingsChipRow(MR.strings.pref_flash_with) {
|
||||||
|
flashColors.map { (labelRes, value) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = flashColor == value,
|
||||||
|
onClick = { flashColorPref.set(value) },
|
||||||
|
label = { Text(stringResource(labelRes)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
inversePrimary = Color(0xFF397E91),
|
inversePrimary = Color(0xFF397E91),
|
||||||
secondary = Color(0xFF81A1C1), // Unread badge
|
secondary = Color(0xFF81A1C1), // Unread badge
|
||||||
onSecondary = Color(0xFF2E3440), // Unread badge text
|
onSecondary = Color(0xFF2E3440), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
|
secondaryContainer = Color(0xFF506275), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
|
onSecondaryContainer = Color(0xFF88C0D0), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF5E81AC), // Downloaded badge
|
tertiary = Color(0xFF5E81AC), // Downloaded badge
|
||||||
onTertiary = Color(0xFF000000), // Downloaded badge text
|
onTertiary = Color(0xFF000000), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF5E81AC),
|
tertiaryContainer = Color(0xFF5E81AC),
|
||||||
@@ -54,7 +54,7 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
inversePrimary = Color(0xFF8CA8CD),
|
inversePrimary = Color(0xFF8CA8CD),
|
||||||
secondary = Color(0xFF81A1C1), // Unread badge
|
secondary = Color(0xFF81A1C1), // Unread badge
|
||||||
onSecondary = Color(0xFF2E3440), // Unread badge text
|
onSecondary = Color(0xFF2E3440), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
|
secondaryContainer = Color(0xFF91B4D7), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
|
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF88C0D0), // Downloaded badge
|
tertiary = Color(0xFF88C0D0), // Downloaded badge
|
||||||
onTertiary = Color(0xFF2E3440), // Downloaded badge text
|
onTertiary = Color(0xFF2E3440), // Downloaded badge text
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ fun TrackInfoDialogHome(
|
|||||||
onNewSearch: (TrackItem) -> Unit,
|
onNewSearch: (TrackItem) -> Unit,
|
||||||
onOpenInBrowser: (TrackItem) -> Unit,
|
onOpenInBrowser: (TrackItem) -> Unit,
|
||||||
onRemoved: (TrackItem) -> Unit,
|
onRemoved: (TrackItem) -> Unit,
|
||||||
|
onCopyLink: (TrackItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -116,6 +117,7 @@ fun TrackInfoDialogHome(
|
|||||||
onNewSearch = { onNewSearch(item) },
|
onNewSearch = { onNewSearch(item) },
|
||||||
onOpenInBrowser = { onOpenInBrowser(item) },
|
onOpenInBrowser = { onOpenInBrowser(item) },
|
||||||
onRemoved = { onRemoved(item) },
|
onRemoved = { onRemoved(item) },
|
||||||
|
onCopyLink = { onCopyLink(item) },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TrackInfoItemEmpty(
|
TrackInfoItemEmpty(
|
||||||
@@ -144,6 +146,7 @@ private fun TrackInfoItem(
|
|||||||
onNewSearch: () -> Unit,
|
onNewSearch: () -> Unit,
|
||||||
onOpenInBrowser: () -> Unit,
|
onOpenInBrowser: () -> Unit,
|
||||||
onRemoved: () -> Unit,
|
onRemoved: () -> Unit,
|
||||||
|
onCopyLink: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column {
|
Column {
|
||||||
@@ -153,6 +156,7 @@ private fun TrackInfoItem(
|
|||||||
TrackLogoIcon(
|
TrackLogoIcon(
|
||||||
tracker = tracker,
|
tracker = tracker,
|
||||||
onClick = onOpenInBrowser,
|
onClick = onOpenInBrowser,
|
||||||
|
onLongClick = onCopyLink,
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -179,6 +183,7 @@ private fun TrackInfoItem(
|
|||||||
TrackInfoItemMenu(
|
TrackInfoItemMenu(
|
||||||
onOpenInBrowser = onOpenInBrowser,
|
onOpenInBrowser = onOpenInBrowser,
|
||||||
onRemoved = onRemoved,
|
onRemoved = onRemoved,
|
||||||
|
onCopyLink = onCopyLink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +292,7 @@ private fun TrackInfoItemEmpty(
|
|||||||
private fun TrackInfoItemMenu(
|
private fun TrackInfoItemMenu(
|
||||||
onOpenInBrowser: () -> Unit,
|
onOpenInBrowser: () -> Unit,
|
||||||
onRemoved: () -> Unit,
|
onRemoved: () -> Unit,
|
||||||
|
onCopyLink: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
|
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
|
||||||
@@ -307,6 +313,13 @@ private fun TrackInfoItemMenu(
|
|||||||
expanded = false
|
expanded = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(MR.strings.action_copy_link)) },
|
||||||
|
onClick = {
|
||||||
|
onCopyLink()
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(MR.strings.action_remove)) },
|
text = { Text(stringResource(MR.strings.action_remove)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
|||||||
onNewSearch = {},
|
onNewSearch = {},
|
||||||
onOpenInBrowser = {},
|
onOpenInBrowser = {},
|
||||||
onRemoved = {},
|
onRemoved = {},
|
||||||
|
onCopyLink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
|||||||
onNewSearch = {},
|
onNewSearch = {},
|
||||||
onOpenInBrowser = {},
|
onOpenInBrowser = {},
|
||||||
onRemoved = {},
|
onRemoved = {},
|
||||||
|
onCopyLink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import tachiyomi.presentation.core.util.clickableNoIndication
|
|||||||
fun TrackLogoIcon(
|
fun TrackLogoIcon(
|
||||||
tracker: Tracker,
|
tracker: Tracker,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val modifier = if (onClick != null) {
|
val modifier = if (onClick != null) {
|
||||||
Modifier.clickableNoIndication(onClick = onClick)
|
Modifier.clickableNoIndication(onClick = onClick, onLongClick = onLongClick)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,7 @@ private fun TrackLogoIconPreviews(
|
|||||||
TrackLogoIcon(
|
TrackLogoIcon(
|
||||||
tracker = tracker,
|
tracker = tracker,
|
||||||
onClick = null,
|
onClick = null,
|
||||||
|
onLongClick = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,30 +199,39 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
override fun newImageLoader(context: Context): ImageLoader {
|
override fun newImageLoader(context: Context): ImageLoader {
|
||||||
return ImageLoader.Builder(this).apply {
|
return ImageLoader.Builder(this).apply {
|
||||||
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
||||||
components {
|
components {
|
||||||
|
// NetworkFetcher.Factory
|
||||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||||
|
// Decoder.Factory
|
||||||
add(TachiyomiImageDecoder.Factory())
|
add(TachiyomiImageDecoder.Factory())
|
||||||
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
|
// Fetcher.Factory
|
||||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
|
||||||
add(MangaKeyer())
|
|
||||||
add(MangaCoverKeyer())
|
|
||||||
add(BufferedSourceFetcher.Factory())
|
add(BufferedSourceFetcher.Factory())
|
||||||
|
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||||
|
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
|
||||||
// SY -->
|
// SY -->
|
||||||
add(PagePreviewKeyer())
|
|
||||||
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
||||||
// SY <--
|
// SY <--
|
||||||
|
// Keyer
|
||||||
|
add(MangaCoverKeyer())
|
||||||
|
add(MangaKeyer())
|
||||||
|
// SY -->
|
||||||
|
add(PagePreviewKeyer())
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||||
|
|
||||||
// Coil spawns a new thread for every image load by default
|
// Coil spawns a new thread for every image load by default
|
||||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8))
|
||||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3))
|
||||||
}.build()
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ package eu.kanade.tachiyomi.data.backup
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class BackupDecoder(
|
class BackupDecoder(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val parser: ProtoBuf = Injekt.get(),
|
private val parser: ProtoBuf = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a potentially-gzipped backup.
|
* Decode a potentially-gzipped backup.
|
||||||
*/
|
*/
|
||||||
@@ -27,13 +29,25 @@ class BackupDecoder(
|
|||||||
require(2)
|
require(2)
|
||||||
}
|
}
|
||||||
val id1id2 = peeked.readShort()
|
val id1id2 = peeked.readShort()
|
||||||
val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes
|
val backupString = when (id1id2.toInt()) {
|
||||||
source.gzip().buffer()
|
0x1f8b -> source.gzip().buffer() // 0x1f8b is gzip magic bytes
|
||||||
} else {
|
MAGIC_JSON_SIGNATURE1, MAGIC_JSON_SIGNATURE2, MAGIC_JSON_SIGNATURE3 -> {
|
||||||
source
|
throw IOException(context.stringResource(MR.strings.invalid_backup_file_json))
|
||||||
|
}
|
||||||
|
else -> source
|
||||||
}.use { it.readByteArray() }
|
}.use { it.readByteArray() }
|
||||||
|
|
||||||
parser.decodeFromByteArray(BackupSerializer, backupString)
|
try {
|
||||||
|
parser.decodeFromByteArray(Backup.serializer(), backupString)
|
||||||
|
} catch (_: SerializationException) {
|
||||||
|
throw IOException(context.stringResource(MR.strings.invalid_backup_file_unknown))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAGIC_JSON_SIGNATURE1 = 0x7b7d // `{}`
|
||||||
|
private const val MAGIC_JSON_SIGNATURE2 = 0x7b22 // `{"`
|
||||||
|
private const val MAGIC_JSON_SIGNATURE3 = 0x7b0a // `{\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import com.hippo.unifile.UniFile
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
@@ -51,6 +52,7 @@ class BackupCreator(
|
|||||||
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||||
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||||
|
private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(),
|
||||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||||
// SY -->
|
// SY -->
|
||||||
private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(),
|
private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(),
|
||||||
@@ -62,47 +64,49 @@ class BackupCreator(
|
|||||||
suspend fun backup(uri: Uri, options: BackupOptions): String {
|
suspend fun backup(uri: Uri, options: BackupOptions): String {
|
||||||
var file: UniFile? = null
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
file = (
|
file = if (isAutoBackup) {
|
||||||
if (isAutoBackup) {
|
// Get dir of file and create
|
||||||
// Get dir of file and create
|
val dir = UniFile.fromUri(context, uri)
|
||||||
val dir = UniFile.fromUri(context, uri)
|
|
||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) }
|
dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(MAX_AUTO_BACKUPS - 1)
|
.drop(MAX_AUTO_BACKUPS - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
dir?.createFile(getFilename())
|
dir?.createFile(getFilename())
|
||||||
} else {
|
} else {
|
||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (file == null || !file.isFile) {
|
if (file == null || !file.isFile) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
val databaseManga = getFavorites.await() /* SY --> */ +
|
val backupManga = backupMangas(
|
||||||
if (options.readEntries) {
|
getFavorites.await() /* SY --> */ +
|
||||||
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
|
if (options.readEntries) {
|
||||||
} else {
|
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
|
||||||
emptyList()
|
} else {
|
||||||
} + getMergedManga.await() // SY <--
|
emptyList()
|
||||||
|
} + getMergedManga.await(), // SY <--
|
||||||
|
options
|
||||||
|
)
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupMangas(databaseManga, options),
|
backupManga = backupManga,
|
||||||
backupCategories = backupCategories(options),
|
backupCategories = backupCategories(options),
|
||||||
backupSources = backupSources(databaseManga),
|
backupSources = backupSources(backupManga),
|
||||||
backupPreferences = backupAppPreferences(options),
|
backupPreferences = backupAppPreferences(options),
|
||||||
|
backupExtensionRepo = backupExtensionRepos(options),
|
||||||
backupSourcePreferences = backupSourcePreferences(options),
|
backupSourcePreferences = backupSourcePreferences(options),
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = backupSavedSearches(),
|
backupSavedSearches = backupSavedSearches(options),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = parser.encodeToByteArray(Backup.serializer(), backup)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
}
|
}
|
||||||
@@ -135,32 +139,42 @@ class BackupCreator(
|
|||||||
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||||
if (!options.categories) return emptyList()
|
if (!options.categories) return emptyList()
|
||||||
|
|
||||||
return categoriesBackupCreator.backupCategories()
|
return categoriesBackupCreator()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||||
return mangaBackupCreator.backupMangas(mangas, options)
|
if (!options.libraryEntries) return emptyList()
|
||||||
|
|
||||||
|
return mangaBackupCreator(mangas, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
||||||
return sourcesBackupCreator.backupSources(mangas)
|
return sourcesBackupCreator(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||||
if (!options.appSettings) return emptyList()
|
if (!options.appSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
|
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
if (!options.sourceSettings) return emptyList()
|
if (!options.sourceSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
|
return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun backupExtensionRepos(options: BackupOptions): List<BackupExtensionRepos> {
|
||||||
|
if (!options.extensionRepoSettings) return emptyList()
|
||||||
|
|
||||||
|
return extensionRepoBackupCreator()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
|
suspend fun backupSavedSearches(options: BackupOptions): List<BackupSavedSearch> {
|
||||||
return savedSearchBackupCreator.backupSavedSearches()
|
if (!options.savedSearches) return emptyList()
|
||||||
|
|
||||||
|
return savedSearchBackupCreator()
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ data class BackupOptions(
|
|||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
// SY -->
|
// SY -->
|
||||||
val customInfo: Boolean = true,
|
val customInfo: Boolean = true,
|
||||||
val readEntries: Boolean = true,
|
val readEntries: Boolean = true,
|
||||||
|
val savedSearches: Boolean = true,
|
||||||
// SY <--
|
// SY <--
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -27,15 +29,17 @@ data class BackupOptions(
|
|||||||
tracking,
|
tracking,
|
||||||
history,
|
history,
|
||||||
appSettings,
|
appSettings,
|
||||||
|
extensionRepoSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
privateSettings,
|
privateSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo,
|
customInfo,
|
||||||
readEntries,
|
readEntries,
|
||||||
|
savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
|
fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val libraryOptions = persistentListOf(
|
val libraryOptions = persistentListOf(
|
||||||
@@ -44,12 +48,6 @@ data class BackupOptions(
|
|||||||
getter = BackupOptions::libraryEntries,
|
getter = BackupOptions::libraryEntries,
|
||||||
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
|
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
|
||||||
label = MR.strings.categories,
|
|
||||||
getter = BackupOptions::categories,
|
|
||||||
setter = { options, enabled -> options.copy(categories = enabled) },
|
|
||||||
enabled = { it.libraryEntries },
|
|
||||||
),
|
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.chapters,
|
label = MR.strings.chapters,
|
||||||
getter = BackupOptions::chapters,
|
getter = BackupOptions::chapters,
|
||||||
@@ -68,6 +66,11 @@ data class BackupOptions(
|
|||||||
setter = { options, enabled -> options.copy(history = enabled) },
|
setter = { options, enabled -> options.copy(history = enabled) },
|
||||||
enabled = { it.libraryEntries },
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.categories,
|
||||||
|
getter = BackupOptions::categories,
|
||||||
|
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||||
|
),
|
||||||
// SY -->
|
// SY -->
|
||||||
Entry(
|
Entry(
|
||||||
label = SYMR.strings.custom_entry_info,
|
label = SYMR.strings.custom_entry_info,
|
||||||
@@ -81,6 +84,11 @@ data class BackupOptions(
|
|||||||
setter = { options, enabled -> options.copy(readEntries = enabled) },
|
setter = { options, enabled -> options.copy(readEntries = enabled) },
|
||||||
enabled = { it.libraryEntries },
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = SYMR.strings.saved_searches,
|
||||||
|
getter = BackupOptions::savedSearches,
|
||||||
|
setter = { options, enabled -> options.copy(savedSearches = enabled) },
|
||||||
|
),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,6 +98,11 @@ data class BackupOptions(
|
|||||||
getter = BackupOptions::appSettings,
|
getter = BackupOptions::appSettings,
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.extensionRepo_settings,
|
||||||
|
getter = BackupOptions::extensionRepoSettings,
|
||||||
|
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||||
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
getter = BackupOptions::sourceSettings,
|
getter = BackupOptions::sourceSettings,
|
||||||
@@ -110,11 +123,13 @@ data class BackupOptions(
|
|||||||
tracking = array[3],
|
tracking = array[3],
|
||||||
history = array[4],
|
history = array[4],
|
||||||
appSettings = array[5],
|
appSettings = array[5],
|
||||||
sourceSettings = array[6],
|
extensionRepoSettings = array[6],
|
||||||
privateSettings = array[7],
|
sourceSettings = array[7],
|
||||||
|
privateSettings = array[8],
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = array[8],
|
customInfo = array[9],
|
||||||
readEntries = array[9],
|
readEntries = array[10],
|
||||||
|
savedSearches = array[11],
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ class CategoriesBackupCreator(
|
|||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun backupCategories(): List<BackupCategory> {
|
suspend operator fun invoke(): List<BackupCategory> {
|
||||||
return getCategories.await()
|
return getCategories.await()
|
||||||
.filterNot(Category::isSystemCategory)
|
.filterNot(Category::isSystemCategory)
|
||||||
.map(backupCategoryMapper)
|
.map(backupCategoryMapper)
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupExtensionReposMapper
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class ExtensionRepoBackupCreator(
|
||||||
|
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(): List<BackupExtensionRepos> {
|
||||||
|
return getExtensionRepos.getAll()
|
||||||
|
.map(backupExtensionReposMapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -34,7 +34,7 @@ class MangaBackupCreator(
|
|||||||
// SY <--
|
// SY <--
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
suspend operator fun invoke(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||||
return mangas.map {
|
return mangas.map {
|
||||||
backupManga(it, options)
|
backupManga(it, options)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -22,12 +22,12 @@ class PreferenceBackupCreator(
|
|||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun backupAppPreferences(includePrivatePreferences: Boolean): List<BackupPreference> {
|
fun createApp(includePrivatePreferences: Boolean): List<BackupPreference> {
|
||||||
return preferenceStore.getAll().toBackupPreferences()
|
return preferenceStore.getAll().toBackupPreferences()
|
||||||
.withPrivatePreferences(includePrivatePreferences)
|
.withPrivatePreferences(includePrivatePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||||
return sourceManager.getCatalogueSources()
|
return sourceManager.getCatalogueSources()
|
||||||
.filterIsInstance<ConfigurableSource>()
|
.filterIsInstance<ConfigurableSource>()
|
||||||
.map {
|
.map {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ class SavedSearchBackupCreator(
|
|||||||
private val handler: DatabaseHandler = Injekt.get()
|
private val handler: DatabaseHandler = Injekt.get()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
|
suspend operator fun invoke(): List<BackupSavedSearch> {
|
||||||
return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) }
|
return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.create.creators
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -11,10 +11,10 @@ class SourcesBackupCreator(
|
|||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
|
||||||
return mangas
|
return mangas
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.map(Manga::source)
|
.map(BackupManga::source)
|
||||||
.distinct()
|
.distinct()
|
||||||
.map(sourceManager::getOrStub)
|
.map(sourceManager::getOrStub)
|
||||||
.map { it.toBackupSource() }
|
.map { it.toBackupSource() }
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializer(forClass = Backup::class)
|
@Suppress("MagicNumber")
|
||||||
object BackupSerializer
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Backup(
|
data class Backup(
|
||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@@ -15,6 +12,7 @@ data class Backup(
|
|||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||||
|
@ProtoNumber(106) var backupExtensionRepo: List<BackupExtensionRepos> = emptyList(),
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
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,
|
||||||
|
@ProtoNumber(2) var name: String,
|
||||||
|
@ProtoNumber(3) var shortName: String?,
|
||||||
|
@ProtoNumber(4) var website: String,
|
||||||
|
@ProtoNumber(5) var signingKeyFingerprint: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupExtensionReposMapper = { repo: ExtensionRepo ->
|
||||||
|
BackupExtensionRepos(
|
||||||
|
baseUrl = repo.baseUrl,
|
||||||
|
name = repo.name,
|
||||||
|
shortName = repo.shortName,
|
||||||
|
website = repo.website,
|
||||||
|
signingKeyFingerprint = repo.signingKeyFingerprint,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@ import android.net.Uri
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.SavedSearchRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.SavedSearchRestorer
|
||||||
@@ -34,6 +36,7 @@ class BackupRestorer(
|
|||||||
|
|
||||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||||
|
private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(),
|
||||||
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
||||||
// SY -->
|
// SY -->
|
||||||
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
||||||
@@ -74,8 +77,11 @@ class BackupRestorer(
|
|||||||
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
||||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||||
|
|
||||||
if (options.library) {
|
if (options.libraryEntries) {
|
||||||
restoreAmount += backup.backupManga.size + 1 // +1 for categories
|
restoreAmount += backup.backupManga.size
|
||||||
|
}
|
||||||
|
if (options.categories) {
|
||||||
|
restoreAmount += 1
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
if (options.savedSearches) {
|
if (options.savedSearches) {
|
||||||
@@ -85,12 +91,15 @@ class BackupRestorer(
|
|||||||
if (options.appSettings) {
|
if (options.appSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
}
|
}
|
||||||
|
if (options.extensionRepoSettings) {
|
||||||
|
restoreAmount += backup.backupExtensionRepo.size
|
||||||
|
}
|
||||||
if (options.sourceSettings) {
|
if (options.sourceSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
if (options.library) {
|
if (options.categories) {
|
||||||
restoreCategories(backup.backupCategories)
|
restoreCategories(backup.backupCategories)
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -104,8 +113,11 @@ class BackupRestorer(
|
|||||||
if (options.sourceSettings) {
|
if (options.sourceSettings) {
|
||||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||||
}
|
}
|
||||||
if (options.library) {
|
if (options.libraryEntries) {
|
||||||
restoreManga(backup.backupManga, backup.backupCategories)
|
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
||||||
|
}
|
||||||
|
if (options.extensionRepoSettings) {
|
||||||
|
restoreExtensionRepos(backup.backupExtensionRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
@@ -114,7 +126,7 @@ class BackupRestorer(
|
|||||||
|
|
||||||
private fun CoroutineScope.restoreCategories(backupCategories: List<BackupCategory>) = launch {
|
private fun CoroutineScope.restoreCategories(backupCategories: List<BackupCategory>) = launch {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
categoriesRestorer.restoreCategories(backupCategories)
|
categoriesRestorer(backupCategories)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
@@ -150,7 +162,7 @@ class BackupRestorer(
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mangaRestorer.restoreManga(it, backupCategories)
|
mangaRestorer.restore(it, backupCategories)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||||
@@ -163,7 +175,7 @@ class BackupRestorer(
|
|||||||
|
|
||||||
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
|
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
preferenceRestorer.restoreAppPreferences(preferences)
|
preferenceRestorer.restoreApp(preferences)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
@@ -176,7 +188,7 @@ class BackupRestorer(
|
|||||||
|
|
||||||
private fun CoroutineScope.restoreSourcePreferences(preferences: List<BackupSourcePreferences>) = launch {
|
private fun CoroutineScope.restoreSourcePreferences(preferences: List<BackupSourcePreferences>) = launch {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
preferenceRestorer.restoreSourcePreferences(preferences)
|
preferenceRestorer.restoreSource(preferences)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
@@ -187,10 +199,33 @@ class BackupRestorer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.restoreExtensionRepos(
|
||||||
|
backupExtensionRepo: List<BackupExtensionRepos>
|
||||||
|
) = launch {
|
||||||
|
backupExtensionRepo
|
||||||
|
.forEach {
|
||||||
|
ensureActive()
|
||||||
|
|
||||||
|
try {
|
||||||
|
extensionRepoRestorer(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
notifier.showRestoreProgress(
|
||||||
|
context.stringResource(MR.strings.extensionRepo_settings),
|
||||||
|
restoreProgress,
|
||||||
|
restoreAmount,
|
||||||
|
isSync,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun writeErrorLog(): File {
|
private fun writeErrorLog(): File {
|
||||||
try {
|
try {
|
||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
val file = context.createFileInCacheDir("mihon_restore_error.txt")
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
|
||||||
data class RestoreOptions(
|
data class RestoreOptions(
|
||||||
val library: Boolean = true,
|
val libraryEntries: Boolean = true,
|
||||||
|
val categories: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
// SY -->
|
// SY -->
|
||||||
val savedSearches: Boolean = true,
|
val savedSearches: Boolean = true,
|
||||||
@@ -15,28 +17,40 @@ data class RestoreOptions(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun asBooleanArray() = booleanArrayOf(
|
fun asBooleanArray() = booleanArrayOf(
|
||||||
library,
|
libraryEntries,
|
||||||
|
categories,
|
||||||
appSettings,
|
appSettings,
|
||||||
|
extensionRepoSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearches
|
savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anyEnabled() = library || appSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */
|
fun canRestore() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val options = persistentListOf(
|
val options = persistentListOf(
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.label_library,
|
label = MR.strings.label_library,
|
||||||
getter = RestoreOptions::library,
|
getter = RestoreOptions::libraryEntries,
|
||||||
setter = { options, enabled -> options.copy(library = enabled) },
|
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.categories,
|
||||||
|
getter = RestoreOptions::categories,
|
||||||
|
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.app_settings,
|
label = MR.strings.app_settings,
|
||||||
getter = RestoreOptions::appSettings,
|
getter = RestoreOptions::appSettings,
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.extensionRepo_settings,
|
||||||
|
getter = RestoreOptions::extensionRepoSettings,
|
||||||
|
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||||
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
getter = RestoreOptions::sourceSettings,
|
getter = RestoreOptions::sourceSettings,
|
||||||
@@ -52,11 +66,13 @@ data class RestoreOptions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun fromBooleanArray(array: BooleanArray) = RestoreOptions(
|
fun fromBooleanArray(array: BooleanArray) = RestoreOptions(
|
||||||
library = array[0],
|
libraryEntries = array[0],
|
||||||
appSettings = array[1],
|
categories = array[1],
|
||||||
sourceSettings = array[2],
|
appSettings = array[2],
|
||||||
|
extensionRepoSettings = array[3],
|
||||||
|
sourceSettings = array[4],
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearches = array[3]
|
savedSearches = array[5]
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-8
@@ -13,7 +13,7 @@ class CategoriesRestorer(
|
|||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
suspend operator fun invoke(backupCategories: List<BackupCategory>) {
|
||||||
if (backupCategories.isNotEmpty()) {
|
if (backupCategories.isNotEmpty()) {
|
||||||
val dbCategories = getCategories.await()
|
val dbCategories = getCategories.await()
|
||||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||||
@@ -21,14 +21,15 @@ class CategoriesRestorer(
|
|||||||
|
|
||||||
val categories = backupCategories
|
val categories = backupCategories
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
.distinctBy { it.name }
|
|
||||||
.map {
|
.map {
|
||||||
val newOrder = nextOrder++
|
val dbCategory = dbCategoriesByName[it.name]
|
||||||
dbCategoriesByName[it.name]
|
if (dbCategory != null) return@map dbCategory
|
||||||
?: handler.awaitOneExecutable {
|
val order = nextOrder++
|
||||||
categoriesQueries.insert(it.name, newOrder, it.flags)
|
handler.awaitOneExecutable {
|
||||||
categoriesQueries.selectLastInsertedRowId()
|
categoriesQueries.insert(it.name, order, it.flags)
|
||||||
}.let { id -> it.toCategory(id).copy(order = newOrder) }
|
categoriesQueries.selectLastInsertedRowId()
|
||||||
|
}
|
||||||
|
.let { id -> it.toCategory(id).copy(order = order) }
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryPreferences.categorizedDisplaySettings().set(
|
libraryPreferences.categorizedDisplaySettings().set(
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class ExtensionRepoRestorer(
|
||||||
|
private val handler: DatabaseHandler = Injekt.get(),
|
||||||
|
private val getExtensionRepos: GetExtensionRepo = Injekt.get()
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
backupRepo: BackupExtensionRepos,
|
||||||
|
) {
|
||||||
|
val dbRepos = getExtensionRepos.getAll()
|
||||||
|
val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint }
|
||||||
|
val existingReposByUrl = dbRepos.associateBy { it.baseUrl }
|
||||||
|
|
||||||
|
val urlExists = existingReposByUrl[backupRepo.baseUrl]
|
||||||
|
val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint]
|
||||||
|
|
||||||
|
if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) {
|
||||||
|
error("Already Exists with different signing key fingerprint")
|
||||||
|
} else if (shaExists != null) {
|
||||||
|
error("${shaExists.name} has the same signing key fingerprint")
|
||||||
|
} else {
|
||||||
|
handler.await {
|
||||||
|
extension_reposQueries.insert(
|
||||||
|
backupRepo.baseUrl,
|
||||||
|
backupRepo.name,
|
||||||
|
backupRepo.shortName,
|
||||||
|
backupRepo.website,
|
||||||
|
backupRepo.signingKeyFingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -68,7 +68,7 @@ class MangaRestorer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restoreManga(
|
suspend fun restore(
|
||||||
backupManga: BackupManga,
|
backupManga: BackupManga,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-2
@@ -22,14 +22,14 @@ class PreferenceRestorer(
|
|||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
fun restoreApp(preferences: List<BackupPreference>) {
|
||||||
restorePreferences(preferences, preferenceStore)
|
restorePreferences(preferences, preferenceStore)
|
||||||
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
BackupCreateJob.setupTask(context)
|
BackupCreateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
fun restoreSource(preferences: List<BackupSourcePreferences>) {
|
||||||
preferences.forEach {
|
preferences.forEach {
|
||||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||||
restorePreferences(it.prefs, sourcePrefs)
|
restorePreferences(it.prefs, sourcePrefs)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import okhttp3.CacheControl
|
|||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.Source
|
import okio.Source
|
||||||
@@ -348,5 +347,7 @@ class MangaCoverFetcher(
|
|||||||
|
|
||||||
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
||||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||||
|
|
||||||
|
private const val HTTP_NOT_MODIFIED = 304
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import okhttp3.CacheControl
|
|||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.Source
|
import okio.Source
|
||||||
@@ -260,5 +259,7 @@ class PagePreviewFetcher(
|
|||||||
companion object {
|
companion object {
|
||||||
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
||||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||||
|
|
||||||
|
private const val HTTP_NOT_MODIFIED = 304
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.coil
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.asCoilImage
|
import coil3.asImage
|
||||||
import coil3.decode.DecodeResult
|
import coil3.decode.DecodeResult
|
||||||
import coil3.decode.DecodeUtils
|
import coil3.decode.DecodeUtils
|
||||||
import coil3.decode.Decoder
|
import coil3.decode.Decoder
|
||||||
@@ -80,7 +80,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
return DecodeResult(
|
return DecodeResult(
|
||||||
image = bitmap.asCoilImage(),
|
image = bitmap.asImage(),
|
||||||
isSampled = sampleSize > 1,
|
isSampled = sampleSize > 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import logcat.LogPriority
|
|||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.storage.extension
|
import tachiyomi.core.common.storage.extension
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
@@ -170,7 +171,7 @@ class DownloadManager(
|
|||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
val files = chapterDir?.listFiles().orEmpty()
|
val files = chapterDir?.listFiles().orEmpty()
|
||||||
.filter { "image" in it.type.orEmpty() }
|
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
if (files.isEmpty()) {
|
||||||
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
|
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
|
||||||
|
|||||||
@@ -553,14 +553,8 @@ class Downloader(
|
|||||||
* @param file the file where the image is already downloaded.
|
* @param file the file where the image is already downloaded.
|
||||||
*/
|
*/
|
||||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
|
||||||
val mime = response.body.contentType()?.run { if (type == "image") "image/$subtype" else null }
|
val mime = response.body.contentType()?.run { if (type == "image") "image/$subtype" else null }
|
||||||
// Else guess from the uri.
|
return ImageUtil.getExtensionFromMimeType(mime) { file.openInputStream() }
|
||||||
?: context.contentResolver.getType(file.uri)
|
|
||||||
// Else read magic numbers.
|
|
||||||
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
|
||||||
|
|
||||||
return ImageUtil.getExtensionFromMimeType(mime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) {
|
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.transformations
|
import coil3.request.transformations
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -65,21 +66,26 @@ class ImageSaver(
|
|||||||
filename: String,
|
filename: String,
|
||||||
data: () -> InputStream,
|
data: () -> InputStream,
|
||||||
): Uri {
|
): Uri {
|
||||||
val pictureDir =
|
val isMimeTypeSupported = MimeTypeMap.getSingleton().hasMimeType(type.mime)
|
||||||
|
|
||||||
|
val pictureDir = if (isMimeTypeSupported) {
|
||||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
} else {
|
||||||
|
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
}
|
||||||
|
|
||||||
val imageLocation = (image.location as Location.Pictures).relativePath
|
val imageLocation = (image.location as Location.Pictures).relativePath
|
||||||
val relativePath = listOf(
|
val relativePath = listOf(
|
||||||
Environment.DIRECTORY_PICTURES,
|
if (isMimeTypeSupported) Environment.DIRECTORY_PICTURES else Environment.DIRECTORY_DOCUMENTS,
|
||||||
context.stringResource(MR.strings.app_name),
|
context.stringResource(MR.strings.app_name),
|
||||||
imageLocation,
|
imageLocation,
|
||||||
).joinToString(File.separator)
|
).joinToString(File.separator)
|
||||||
|
|
||||||
val contentValues = contentValuesOf(
|
val contentValues = contentValuesOf(
|
||||||
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
MediaStore.MediaColumns.RELATIVE_PATH to relativePath,
|
||||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename,
|
||||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
MediaStore.MediaColumns.MIME_TYPE to type.mime,
|
||||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond,
|
MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond,
|
||||||
)
|
)
|
||||||
|
|
||||||
val picture = findUriOrDefault(relativePath, filename) {
|
val picture = findUriOrDefault(relativePath, filename) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||||
@@ -85,6 +84,7 @@ class SyncManager(
|
|||||||
chapters = syncOptions.chapters,
|
chapters = syncOptions.chapters,
|
||||||
tracking = syncOptions.tracking,
|
tracking = syncOptions.tracking,
|
||||||
history = syncOptions.history,
|
history = syncOptions.history,
|
||||||
|
extensionRepoSettings = syncOptions.extensionRepoSettings,
|
||||||
appSettings = syncOptions.appSettings,
|
appSettings = syncOptions.appSettings,
|
||||||
sourceSettings = syncOptions.sourceSettings,
|
sourceSettings = syncOptions.sourceSettings,
|
||||||
privateSettings = syncOptions.privateSettings,
|
privateSettings = syncOptions.privateSettings,
|
||||||
@@ -92,19 +92,22 @@ class SyncManager(
|
|||||||
// SY -->
|
// SY -->
|
||||||
customInfo = syncOptions.customInfo,
|
customInfo = syncOptions.customInfo,
|
||||||
readEntries = syncOptions.readEntries,
|
readEntries = syncOptions.readEntries,
|
||||||
|
savedSearches = syncOptions.savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
logcat(LogPriority.DEBUG) { "Begin create backup" }
|
logcat(LogPriority.DEBUG) { "Begin create backup" }
|
||||||
|
val backupManga = backupCreator.backupMangas(databaseManga, backupOptions)
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
|
backupManga = backupManga,
|
||||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||||
backupSources = backupCreator.backupSources(databaseManga),
|
backupSources = backupCreator.backupSources(backupManga),
|
||||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||||
|
backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = backupCreator.backupSavedSearches(),
|
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
logcat(LogPriority.DEBUG) { "End create backup" }
|
logcat(LogPriority.DEBUG) { "End create backup" }
|
||||||
@@ -175,6 +178,7 @@ class SyncManager(
|
|||||||
backupSources = remoteBackup.backupSources,
|
backupSources = remoteBackup.backupSources,
|
||||||
backupPreferences = remoteBackup.backupPreferences,
|
backupPreferences = remoteBackup.backupPreferences,
|
||||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||||
|
backupExtensionRepo = remoteBackup.backupExtensionRepo,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||||
@@ -199,7 +203,8 @@ class SyncManager(
|
|||||||
options = RestoreOptions(
|
options = RestoreOptions(
|
||||||
appSettings = true,
|
appSettings = true,
|
||||||
sourceSettings = true,
|
sourceSettings = true,
|
||||||
library = true,
|
libraryEntries = true,
|
||||||
|
extensionRepoSettings = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -214,7 +219,7 @@ class SyncManager(
|
|||||||
val cacheFile = File(context.cacheDir, "tachiyomi_sync_data.proto.gz")
|
val cacheFile = File(context.cacheDir, "tachiyomi_sync_data.proto.gz")
|
||||||
return try {
|
return try {
|
||||||
cacheFile.outputStream().use { output ->
|
cacheFile.outputStream().use { output ->
|
||||||
output.write(ProtoBuf.encodeToByteArray(BackupSerializer, backup))
|
output.write(ProtoBuf.encodeToByteArray(Backup.serializer(), backup))
|
||||||
Uri.fromFile(cacheFile)
|
Uri.fromFile(cacheFile)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
|||||||
+30
-128
@@ -10,7 +10,6 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeToken
|
|||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
|
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
|
||||||
import com.google.api.client.http.ByteArrayContent
|
|
||||||
import com.google.api.client.http.InputStreamContent
|
import com.google.api.client.http.InputStreamContent
|
||||||
import com.google.api.client.http.javanet.NetHttpTransport
|
import com.google.api.client.http.javanet.NetHttpTransport
|
||||||
import com.google.api.client.json.JsonFactory
|
import com.google.api.client.json.JsonFactory
|
||||||
@@ -20,11 +19,9 @@ import com.google.api.services.drive.DriveScopes
|
|||||||
import com.google.api.services.drive.model.File
|
import com.google.api.services.drive.model.File
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import kotlinx.serialization.json.encodeToStream
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.logcat
|
import logcat.logcat
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
@@ -37,7 +34,6 @@ import uy.kohesive.injekt.api.get
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.PipedInputStream
|
import java.io.PipedInputStream
|
||||||
import java.io.PipedOutputStream
|
import java.io.PipedOutputStream
|
||||||
import java.time.Instant
|
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
@@ -64,12 +60,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
|
|
||||||
private val appName = context.stringResource(MR.strings.app_name)
|
private val appName = context.stringResource(MR.strings.app_name)
|
||||||
|
|
||||||
private val remoteFileName = "${appName}_sync_data.gz"
|
private val remoteFileName = "${appName}_sync.proto.gz"
|
||||||
|
|
||||||
private val lockFileName = "${appName}_sync.lock"
|
|
||||||
|
|
||||||
private val googleDriveService = GoogleDriveService(context)
|
private val googleDriveService = GoogleDriveService(context)
|
||||||
|
|
||||||
|
private val protoBuf: ProtoBuf = Injekt.get()
|
||||||
|
|
||||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
beforeSync()
|
beforeSync()
|
||||||
|
|
||||||
@@ -107,64 +103,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun beforeSync() {
|
private suspend fun beforeSync() {
|
||||||
try {
|
googleDriveService.refreshToken()
|
||||||
googleDriveService.refreshToken()
|
|
||||||
val drive = googleDriveService.driveService
|
|
||||||
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
|
||||||
|
|
||||||
var backoff = 1000L
|
|
||||||
var retries = 0 // Retry counter
|
|
||||||
val maxRetries = 10 // Maximum number of retries
|
|
||||||
|
|
||||||
while (retries < maxRetries) {
|
|
||||||
val lockFiles = findLockFile(drive)
|
|
||||||
logcat(LogPriority.DEBUG) { "Found ${lockFiles.size} lock file(s)" }
|
|
||||||
|
|
||||||
when {
|
|
||||||
lockFiles.isEmpty() -> {
|
|
||||||
logcat(LogPriority.DEBUG) { "No lock file found, creating a new one" }
|
|
||||||
createLockFile(drive)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lockFiles.size == 1 -> {
|
|
||||||
val lockFile = lockFiles.first()
|
|
||||||
val createdTime = Instant.parse(lockFile.createdTime.toString())
|
|
||||||
val ageMinutes = java.time.Duration.between(createdTime, Instant.now()).toMinutes()
|
|
||||||
logcat(LogPriority.DEBUG) { "Lock file age: $ageMinutes minutes" }
|
|
||||||
if (ageMinutes <= 3) {
|
|
||||||
logcat(LogPriority.DEBUG) { "Lock file is new, proceeding with sync" }
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
logcat(LogPriority.DEBUG) { "Lock file is old, deleting and creating a new one" }
|
|
||||||
deleteLockFile(drive)
|
|
||||||
createLockFile(drive)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logcat(LogPriority.DEBUG) { "Multiple lock files found, applying backoff" }
|
|
||||||
delay(backoff) // Apply backoff strategy
|
|
||||||
backoff = (backoff * 2).coerceAtMost(16000L)
|
|
||||||
logcat(LogPriority.DEBUG) { "Backoff increased to $backoff milliseconds" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
retries++ // Increment retry counter
|
|
||||||
logcat(LogPriority.DEBUG) { "Loop iteration complete, retry count: $retries, backoff time: $backoff" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retries >= maxRetries) {
|
|
||||||
logcat(LogPriority.ERROR) { "Max retries reached, exiting sync process" }
|
|
||||||
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": Max retries reached.")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" }
|
|
||||||
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pullSyncData(): SyncData? {
|
private fun pullSyncData(): SyncData? {
|
||||||
val drive = googleDriveService.driveService ?:
|
val drive = googleDriveService.driveService
|
||||||
throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
|
|
||||||
val fileList = getAppDataFileList(drive)
|
val fileList = getAppDataFileList(drive)
|
||||||
if (fileList.isEmpty()) {
|
if (fileList.isEmpty()) {
|
||||||
@@ -178,7 +122,10 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
try {
|
try {
|
||||||
drive.files().get(gdriveFileId).executeMediaAsInputStream().use { inputStream ->
|
drive.files().get(gdriveFileId).executeMediaAsInputStream().use { inputStream ->
|
||||||
GZIPInputStream(inputStream).use { gzipInputStream ->
|
GZIPInputStream(inputStream).use { gzipInputStream ->
|
||||||
return Json.decodeFromStream(SyncData.serializer(), gzipInputStream)
|
val byteArray = gzipInputStream.readBytes()
|
||||||
|
val backup = protoBuf.decodeFromByteArray(Backup.serializer(), byteArray)
|
||||||
|
val deviceId = fileList[0].appProperties["deviceId"] ?: ""
|
||||||
|
return SyncData(deviceId = deviceId, backup = backup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -192,29 +139,40 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
|
|
||||||
val fileList = getAppDataFileList(drive)
|
val fileList = getAppDataFileList(drive)
|
||||||
|
val backup = syncData.backup ?: return
|
||||||
|
|
||||||
|
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
PipedOutputStream().use { pos ->
|
PipedOutputStream().use { pos ->
|
||||||
PipedInputStream(pos).use { pis ->
|
PipedInputStream(pos).use { pis ->
|
||||||
withIOContext {
|
withIOContext {
|
||||||
// Start a coroutine or a background thread to write JSON to the PipedOutputStream
|
|
||||||
launch {
|
launch {
|
||||||
GZIPOutputStream(pos).use { gzipOutputStream ->
|
GZIPOutputStream(pos).use { gzipOutputStream ->
|
||||||
Json.encodeToStream(SyncData.serializer(), syncData, gzipOutputStream)
|
gzipOutputStream.write(byteArray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mediaContent = InputStreamContent("application/octet-stream", pis)
|
||||||
|
|
||||||
if (fileList.isNotEmpty()) {
|
if (fileList.isNotEmpty()) {
|
||||||
val fileId = fileList[0].id
|
val fileId = fileList[0].id
|
||||||
val mediaContent = InputStreamContent("application/gzip", pis)
|
val fileMetadata = File().apply {
|
||||||
drive.files().update(fileId, null, mediaContent).execute()
|
name = remoteFileName
|
||||||
|
mimeType = "application/octet-stream"
|
||||||
|
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 {
|
} else {
|
||||||
val fileMetadata = File().apply {
|
val fileMetadata = File().apply {
|
||||||
name = remoteFileName
|
name = remoteFileName
|
||||||
mimeType = "application/gzip"
|
mimeType = "application/octet-stream"
|
||||||
parents = listOf("appDataFolder")
|
parents = listOf("appDataFolder")
|
||||||
|
appProperties = mapOf("deviceId" to syncData.deviceId)
|
||||||
}
|
}
|
||||||
val mediaContent = InputStreamContent("application/gzip", pis)
|
|
||||||
val uploadedFile = drive.files().create(fileMetadata, mediaContent)
|
val uploadedFile = drive.files().create(fileMetadata, mediaContent)
|
||||||
.setFields("id")
|
.setFields("id")
|
||||||
.execute()
|
.execute()
|
||||||
@@ -228,12 +186,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
private fun getAppDataFileList(drive: Drive): MutableList<File> {
|
private fun getAppDataFileList(drive: Drive): MutableList<File> {
|
||||||
try {
|
try {
|
||||||
// Search for the existing file by name in the appData folder
|
// Search for the existing file by name in the appData folder
|
||||||
val query = "mimeType='application/gzip' and name = '$remoteFileName'"
|
val query = "mimeType='application/x-gzip' and name = '$remoteFileName'"
|
||||||
val fileList = drive.files()
|
val fileList = drive.files()
|
||||||
.list()
|
.list()
|
||||||
.setSpaces("appDataFolder")
|
.setSpaces("appDataFolder")
|
||||||
.setQ(query)
|
.setQ(query)
|
||||||
.setFields("files(id, name, createdTime)")
|
.setFields("files(id, name, createdTime, appProperties)")
|
||||||
.execute()
|
.execute()
|
||||||
.files
|
.files
|
||||||
logcat { "AppData folder file list: $fileList" }
|
logcat { "AppData folder file list: $fileList" }
|
||||||
@@ -245,62 +203,6 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLockFile(drive: Drive) {
|
|
||||||
try {
|
|
||||||
val fileMetadata = File().apply {
|
|
||||||
name = lockFileName
|
|
||||||
mimeType = "text/plain"
|
|
||||||
parents = listOf("appDataFolder")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an empty content to upload as the lock file
|
|
||||||
val emptyContent = ByteArrayContent.fromString("text/plain", "")
|
|
||||||
|
|
||||||
val file = drive.files().create(fileMetadata, emptyContent)
|
|
||||||
.setFields("id, name, createdTime")
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
logcat { "Created lock file with ID: ${file.id}" }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error creating lock file" }
|
|
||||||
throw Exception(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findLockFile(drive: Drive): MutableList<File> {
|
|
||||||
return try {
|
|
||||||
val query = "mimeType='text/plain' and name = '$lockFileName'"
|
|
||||||
val fileList = drive.files()
|
|
||||||
.list()
|
|
||||||
.setSpaces("appDataFolder")
|
|
||||||
.setQ(query)
|
|
||||||
.setFields("files(id, name, createdTime)")
|
|
||||||
.execute().files
|
|
||||||
logcat { "Lock file search result: $fileList" }
|
|
||||||
fileList
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error finding lock file" }
|
|
||||||
mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteLockFile(drive: Drive) {
|
|
||||||
try {
|
|
||||||
val lockFiles = findLockFile(drive)
|
|
||||||
|
|
||||||
if (lockFiles.isNotEmpty()) {
|
|
||||||
for (file in lockFiles) {
|
|
||||||
drive.files().delete(file.id).execute()
|
|
||||||
logcat { "Deleted lock file with ID: ${file.id}" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logcat { "No lock file found to delete." }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" }
|
|
||||||
throw Exception(context.stringResource(SYMR.strings.error_deleting_google_drive_lock_file), e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus {
|
suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus {
|
||||||
val drive = googleDriveService.driveService
|
val drive = googleDriveService.driveService
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.sync.service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
@@ -104,7 +103,7 @@ class SyncYomiSyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val backup = protoBuf.decodeFromByteArray(BackupSerializer, byteArray)
|
val backup = protoBuf.decodeFromByteArray(Backup.serializer(), byteArray)
|
||||||
return Pair(SyncData(backup = backup), newETag)
|
return Pair(SyncData(backup = backup), newETag)
|
||||||
} catch (_: SerializationException) {
|
} catch (_: SerializationException) {
|
||||||
logcat(LogPriority.INFO) {
|
logcat(LogPriority.INFO) {
|
||||||
@@ -147,7 +146,7 @@ class SyncYomiSyncService(
|
|||||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val byteArray = protoBuf.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import eu.kanade.domain.track.service.TrackPreferences
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -53,6 +55,15 @@ abstract class BaseTracker(
|
|||||||
get() = getUsername().isNotEmpty() &&
|
get() = getUsername().isNotEmpty() &&
|
||||||
getPassword().isNotEmpty()
|
getPassword().isNotEmpty()
|
||||||
|
|
||||||
|
override val isLoggedInFlow: Flow<Boolean> by lazy {
|
||||||
|
combine(
|
||||||
|
trackPreferences.trackUsername(this).changes(),
|
||||||
|
trackPreferences.trackPassword(this).changes(),
|
||||||
|
) { username, password ->
|
||||||
|
username.isNotEmpty() && password.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUsername() = trackPreferences.trackUsername(this).get()
|
override fun getUsername() = trackPreferences.trackUsername(this).get()
|
||||||
|
|
||||||
override fun getPassword() = trackPreferences.trackPassword(this).get()
|
override fun getPassword() = trackPreferences.trackPassword(this).get()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
@@ -61,6 +62,8 @@ interface Tracker {
|
|||||||
|
|
||||||
val isLoggedIn: Boolean
|
val isLoggedIn: Boolean
|
||||||
|
|
||||||
|
val isLoggedInFlow: Flow<Boolean>
|
||||||
|
|
||||||
fun getUsername(): String
|
fun getUsername(): String
|
||||||
|
|
||||||
fun getPassword(): String
|
fun getPassword(): String
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
|||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
|
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
class TrackerManager {
|
class TrackerManager {
|
||||||
|
|
||||||
@@ -40,5 +41,13 @@ class TrackerManager {
|
|||||||
|
|
||||||
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
||||||
|
|
||||||
|
fun loggedInTrackersFlow() = combine(trackers.map { it.isLoggedInFlow }) {
|
||||||
|
it.mapIndexedNotNull { index, isLoggedIn ->
|
||||||
|
if (isLoggedIn) trackers[index] else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun get(id: Long) = trackers.find { it.id == id }
|
fun get(id: Long) = trackers.find { it.id == id }
|
||||||
|
|
||||||
|
fun getAll(ids: Set<Long>) = trackers.filter { it.id in ids }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,10 +270,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
private const val clientSecret =
|
private const val clientSecret =
|
||||||
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
|
||||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
private const val baseUrl = "https://kitsu.app/api/edge/"
|
||||||
private const val loginUrl = "https://kitsu.io/api/oauth/token"
|
private const val loginUrl = "https://kitsu.app/api/oauth/token"
|
||||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
private const val baseMangaUrl = "https://kitsu.app/manga/"
|
||||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/"
|
private const val algoliaKeyUrl = "https://kitsu.app/api/edge/algolia-keys/media/"
|
||||||
|
|
||||||
private const val algoliaUrl =
|
private const val algoliaUrl =
|
||||||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
||||||
|
|||||||
@@ -129,12 +129,7 @@ class MyAnimeListApi(
|
|||||||
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
||||||
publishing_type =
|
publishing_type =
|
||||||
obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
||||||
start_date = try {
|
start_date = obj["start_date"]?.jsonPrimitive?.content ?: ""
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
outputDf.format(obj["start_date"]!!)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ abstract class Installer(private val service: Service) {
|
|||||||
}
|
}
|
||||||
val nextEntry = queue.first()
|
val nextEntry = queue.first()
|
||||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||||
queue.removeFirst()
|
queue.removeAt(0)
|
||||||
processEntry(nextEntry)
|
processEntry(nextEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
emit(downloadStatus)
|
emit(downloadStatus)
|
||||||
|
|
||||||
// Stop polling when the download fails or finishes
|
// Stop polling when the download fails or finishes
|
||||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
if (
|
||||||
|
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
||||||
|
downloadStatus == DownloadManager.STATUS_FAILED
|
||||||
|
) {
|
||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class AndroidSourceManager(
|
|||||||
private val sourceRepository: StubSourceRepository,
|
private val sourceRepository: StubSourceRepository,
|
||||||
) : SourceManager {
|
) : SourceManager {
|
||||||
|
|
||||||
|
private val _isInitialized = MutableStateFlow(false)
|
||||||
|
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
@@ -189,9 +192,6 @@ class AndroidSourceManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val _isInitialized = MutableStateFlow(false)
|
|
||||||
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
|
||||||
|
|
||||||
override fun getVisibleOnlineSources() = sourcesMapFlow.value.values
|
override fun getVisibleOnlineSources() = sourcesMapFlow.value.values
|
||||||
.filterIsInstance<HttpSource>()
|
.filterIsInstance<HttpSource>()
|
||||||
.filter {
|
.filter {
|
||||||
|
|||||||
+2
@@ -74,6 +74,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
|
|
||||||
binding.skipStep.isChecked = preferences.skipPreMigration().get()
|
binding.skipStep.isChecked = preferences.skipPreMigration().get()
|
||||||
binding.HideNotFoundManga.isChecked = preferences.hideNotFoundMigration().get()
|
binding.HideNotFoundManga.isChecked = preferences.hideNotFoundMigration().get()
|
||||||
|
binding.OnlyShowUpdates.isChecked = preferences.showOnlyUpdatesMigration().get()
|
||||||
binding.skipStep.setOnCheckedChangeListener { _, isChecked ->
|
binding.skipStep.setOnCheckedChangeListener { _, isChecked ->
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
binding.root.context.toast(
|
binding.root.context.toast(
|
||||||
@@ -86,6 +87,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
binding.migrateBtn.setOnClickListener {
|
binding.migrateBtn.setOnClickListener {
|
||||||
preferences.skipPreMigration().set(binding.skipStep.isChecked)
|
preferences.skipPreMigration().set(binding.skipStep.isChecked)
|
||||||
preferences.hideNotFoundMigration().set(binding.HideNotFoundManga.isChecked)
|
preferences.hideNotFoundMigration().set(binding.HideNotFoundManga.isChecked)
|
||||||
|
preferences.showOnlyUpdatesMigration().set(binding.OnlyShowUpdates.isChecked)
|
||||||
onStartMigration.value(
|
onStartMigration.value(
|
||||||
if (binding.useSmartSearch.isChecked && binding.extraSearchParamText.text.isNotBlank()) {
|
if (binding.useSmartSearch.isChecked && binding.extraSearchParamText.text.isNotBlank()) {
|
||||||
binding.extraSearchParamText.toString()
|
binding.extraSearchParamText.toString()
|
||||||
|
|||||||
+7
@@ -94,6 +94,7 @@ class MigrationListScreenModel(
|
|||||||
val manualMigrations = MutableStateFlow(0)
|
val manualMigrations = MutableStateFlow(0)
|
||||||
|
|
||||||
val hideNotFound = preferences.hideNotFoundMigration().get()
|
val hideNotFound = preferences.hideNotFoundMigration().get()
|
||||||
|
val showOnlyUpdates = preferences.showOnlyUpdatesMigration().get()
|
||||||
|
|
||||||
val navigateOut = MutableSharedFlow<Unit>()
|
val navigateOut = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
@@ -313,6 +314,12 @@ class MigrationListScreenModel(
|
|||||||
if (result == null && hideNotFound) {
|
if (result == null && hideNotFound) {
|
||||||
removeManga(manga)
|
removeManga(manga)
|
||||||
}
|
}
|
||||||
|
if (result != null && showOnlyUpdates &&
|
||||||
|
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
||||||
|
) {
|
||||||
|
removeManga(manga)
|
||||||
|
}
|
||||||
|
|
||||||
sourceFinished()
|
sourceFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
import eu.kanade.domain.manga.model.toDomainManga
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.util.ioCoroutineScope
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import tachiyomi.core.common.preference.toggle
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -39,6 +41,7 @@ abstract class SearchScreenModel(
|
|||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
|
private val preferences: SourcePreferences = Injekt.get(),
|
||||||
) : StateScreenModel<SearchScreenModel.State>(initialState) {
|
) : StateScreenModel<SearchScreenModel.State>(initialState) {
|
||||||
|
|
||||||
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||||
@@ -61,6 +64,14 @@ abstract class SearchScreenModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
screenModelScope.launch {
|
||||||
|
preferences.globalSearchFilterState().changes().collectLatest { state ->
|
||||||
|
mutableState.update { it.copy(onlyShowHasResults = state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
|
fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
|
||||||
return produceState(initialValue = initialManga) {
|
return produceState(initialValue = initialManga) {
|
||||||
@@ -111,7 +122,7 @@ abstract class SearchScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toggleFilterResults() {
|
fun toggleFilterResults() {
|
||||||
mutableState.update { it.copy(onlyShowHasResults = !it.onlyShowHasResults) }
|
preferences.globalSearchFilterState().toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.debounce
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -177,18 +178,18 @@ class LibraryScreenModel(
|
|||||||
::Pair,
|
::Pair,
|
||||||
),
|
),
|
||||||
// SY <--
|
// SY <--
|
||||||
) { searchQuery, library, tracks, (loggedInTrackers, _), (groupType, sort) ->
|
) { searchQuery, library, tracks, (trackingFiler, _), (groupType, sort) ->
|
||||||
library
|
library
|
||||||
// SY -->
|
// SY -->
|
||||||
.applyGrouping(groupType)
|
.applyGrouping(groupType)
|
||||||
// SY <--
|
// SY <--
|
||||||
.applyFilters(tracks, loggedInTrackers)
|
.applyFilters(tracks, trackingFiler)
|
||||||
.applySort(tracks, /* SY --> */sort.takeIf { groupType != LibraryGroup.BY_DEFAULT } /* SY <-- */)
|
.applySort(tracks, trackingFiler.keys,/* SY --> */sort.takeIf { groupType != LibraryGroup.BY_DEFAULT } /* SY <-- */)
|
||||||
.mapValues { (_, value) ->
|
.mapValues { (_, value) ->
|
||||||
if (searchQuery != null) {
|
if (searchQuery != null) {
|
||||||
// Filter query
|
// Filter query
|
||||||
// SY -->
|
// SY -->
|
||||||
filterLibrary(value, searchQuery, loggedInTrackers)
|
filterLibrary(value, searchQuery, trackingFiler)
|
||||||
// SY <--
|
// SY <--
|
||||||
} else {
|
} else {
|
||||||
// Don't do anything
|
// Don't do anything
|
||||||
@@ -277,9 +278,10 @@ class LibraryScreenModel(
|
|||||||
/**
|
/**
|
||||||
* Applies library filters to the given map of manga.
|
* Applies library filters to the given map of manga.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
private suspend fun LibraryMap.applyFilters(
|
private suspend fun LibraryMap.applyFilters(
|
||||||
trackMap: Map<Long, List<Track>>,
|
trackMap: Map<Long, List<Track>>,
|
||||||
loggedInTrackers: Map<Long, TriState>,
|
trackingFiler: Map<Long, TriState>,
|
||||||
): LibraryMap {
|
): LibraryMap {
|
||||||
val prefs = getLibraryItemPreferencesFlow().first()
|
val prefs = getLibraryItemPreferencesFlow().first()
|
||||||
val downloadedOnly = prefs.globalFilterDownloaded
|
val downloadedOnly = prefs.globalFilterDownloaded
|
||||||
@@ -291,10 +293,10 @@ class LibraryScreenModel(
|
|||||||
val filterCompleted = prefs.filterCompleted
|
val filterCompleted = prefs.filterCompleted
|
||||||
val filterIntervalCustom = prefs.filterIntervalCustom
|
val filterIntervalCustom = prefs.filterIntervalCustom
|
||||||
|
|
||||||
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty()
|
val isNotLoggedInAnyTrack = trackingFiler.isEmpty()
|
||||||
|
|
||||||
val excludedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
|
val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
|
||||||
val includedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
|
val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
|
||||||
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
|
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -371,9 +373,11 @@ class LibraryScreenModel(
|
|||||||
/**
|
/**
|
||||||
* Applies library sorting to the given map of manga.
|
* Applies library sorting to the given map of manga.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
private fun LibraryMap.applySort(
|
private fun LibraryMap.applySort(
|
||||||
// Map<MangaId, List<Track>>
|
// Map<MangaId, List<Track>>
|
||||||
trackMap: Map<Long, List<Track>>,
|
trackMap: Map<Long, List<Track>>,
|
||||||
|
loggedInTrackerIds: Set<Long>,
|
||||||
/* SY --> */
|
/* SY --> */
|
||||||
groupSort: LibrarySort? = null, /* SY <-- */
|
groupSort: LibrarySort? = null, /* SY <-- */
|
||||||
): LibraryMap {
|
): LibraryMap {
|
||||||
@@ -397,7 +401,7 @@ class LibraryScreenModel(
|
|||||||
|
|
||||||
val defaultTrackerScoreSortValue = -1.0
|
val defaultTrackerScoreSortValue = -1.0
|
||||||
val trackerScores by lazy {
|
val trackerScores by lazy {
|
||||||
val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id }
|
val trackerMap = trackerManager.getAll(loggedInTrackerIds).associateBy { e -> e.id }
|
||||||
trackMap.mapValues { entry ->
|
trackMap.mapValues { entry ->
|
||||||
when {
|
when {
|
||||||
entry.value.isEmpty() -> null
|
entry.value.isEmpty() -> null
|
||||||
@@ -596,18 +600,17 @@ class LibraryScreenModel(
|
|||||||
* @return map of track id with the filter value
|
* @return map of track id with the filter value
|
||||||
*/
|
*/
|
||||||
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
|
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
|
||||||
val loggedInTrackers = trackerManager.loggedInTrackers()
|
return trackerManager.loggedInTrackersFlow().flatMapLatest { loggedInTrackers ->
|
||||||
return if (loggedInTrackers.isNotEmpty()) {
|
if (loggedInTrackers.isEmpty()) return@flatMapLatest flowOf(emptyMap())
|
||||||
val prefFlows = loggedInTrackers
|
|
||||||
.map { libraryPreferences.filterTracking(it.id.toInt()).changes() }
|
val prefFlows = loggedInTrackers.map { tracker ->
|
||||||
.toTypedArray()
|
libraryPreferences.filterTracking(tracker.id.toInt()).changes()
|
||||||
combine(*prefFlows) {
|
}
|
||||||
|
combine(prefFlows) {
|
||||||
loggedInTrackers
|
loggedInTrackers
|
||||||
.mapIndexed { index, tracker -> tracker.id to it[index] }
|
.mapIndexed { index, tracker -> tracker.id to it[index] }
|
||||||
.toMap()
|
.toMap()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
flowOf(emptyMap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.TriState
|
import tachiyomi.core.common.preference.TriState
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
@@ -18,17 +20,22 @@ import tachiyomi.domain.library.model.LibrarySort
|
|||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class LibrarySettingsScreenModel(
|
class LibrarySettingsScreenModel(
|
||||||
val preferences: BasePreferences = Injekt.get(),
|
val preferences: BasePreferences = Injekt.get(),
|
||||||
val libraryPreferences: LibraryPreferences = Injekt.get(),
|
val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
private val setDisplayMode: SetDisplayMode = Injekt.get(),
|
private val setDisplayMode: SetDisplayMode = Injekt.get(),
|
||||||
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
|
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
|
||||||
private val trackerManager: TrackerManager = Injekt.get(),
|
trackerManager: TrackerManager = Injekt.get(),
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
val trackers
|
val trackersFlow = trackerManager.loggedInTrackersFlow()
|
||||||
get() = trackerManager.trackers.filter { it.isLoggedIn }
|
.stateIn(
|
||||||
|
scope = screenModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
|
||||||
|
initialValue = trackerManager.loggedInTrackers()
|
||||||
|
)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val grouping by libraryPreferences.groupLibraryBy().asState(screenModelScope)
|
val grouping by libraryPreferences.groupLibraryBy().asState(screenModelScope)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.size.Size
|
import coil3.size.Size
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class MangaScreen(
|
|||||||
)
|
)
|
||||||
}.takeIf { isHttpSource },
|
}.takeIf { isHttpSource },
|
||||||
onTrackingClicked = {
|
onTrackingClicked = {
|
||||||
if (screenModel.loggedInTrackers.isEmpty()) {
|
if (!successState.hasLoggedInTrackers) {
|
||||||
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
|
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
|
||||||
} else {
|
} else {
|
||||||
screenModel.showTrackDialog()
|
screenModel.showTrackDialog()
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ import tachiyomi.domain.manga.repository.MangaRepository
|
|||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
import tachiyomi.domain.track.model.Track
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.source.local.LocalSource
|
import tachiyomi.source.local.LocalSource
|
||||||
@@ -187,8 +188,6 @@ class MangaScreenModel(
|
|||||||
private val successState: State.Success?
|
private val successState: State.Success?
|
||||||
get() = state.value as? State.Success
|
get() = state.value as? State.Success
|
||||||
|
|
||||||
val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } }
|
|
||||||
|
|
||||||
val manga: Manga?
|
val manga: Manga?
|
||||||
get() = successState?.manga
|
get() = successState?.manga
|
||||||
|
|
||||||
@@ -1490,45 +1489,56 @@ class MangaScreenModel(
|
|||||||
val manga = state?.manga ?: return
|
val manga = state?.manga ?: return
|
||||||
|
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
getTracks.subscribe(manga.id)
|
combine(
|
||||||
.catch { logcat(LogPriority.ERROR, it) }
|
getTracks.subscribe(manga.id)
|
||||||
.map { tracks ->
|
// SY -->
|
||||||
loggedInTrackers
|
.map { trackItems ->
|
||||||
// Map to TrackItem
|
if (manga.source in mangaDexSourceIds || state.mergedData?.manga?.values.orEmpty().any {
|
||||||
.map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) }
|
it.source in mangaDexSourceIds
|
||||||
// Show only if the service supports this manga's source
|
|
||||||
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
|
|
||||||
}
|
|
||||||
// SY -->
|
|
||||||
.map { trackItems ->
|
|
||||||
if (manga.source in mangaDexSourceIds || state.mergedData?.manga?.values.orEmpty().any {
|
|
||||||
it.source in mangaDexSourceIds
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val mdTrack = trackItems.firstOrNull { it.tracker is MdList }
|
|
||||||
when {
|
|
||||||
mdTrack == null -> {
|
|
||||||
trackItems
|
|
||||||
}
|
}
|
||||||
mdTrack.track == null -> {
|
) {
|
||||||
trackItems - mdTrack + createMdListTrack()
|
val mdTrack = trackItems.firstOrNull { it.trackerId == TrackerManager.MDLIST }
|
||||||
|
when {
|
||||||
|
trackerManager.mdList.isLoggedIn && mdTrack == null -> {
|
||||||
|
trackItems + createMdListTrack()
|
||||||
|
}
|
||||||
|
else -> trackItems
|
||||||
}
|
}
|
||||||
else -> trackItems
|
} else {
|
||||||
|
trackItems
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
trackItems
|
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
|
.catch { logcat(LogPriority.ERROR, it) },
|
||||||
|
trackerManager.loggedInTrackersFlow(),
|
||||||
|
) { mangaTracks, loggedInTrackers ->
|
||||||
|
// Show only if the service supports this manga's source
|
||||||
|
val supportedTrackers = loggedInTrackers.filter { (it as? EnhancedTracker)?.accept(source!!) ?: true }
|
||||||
|
val supportedTrackerIds = supportedTrackers.map { it.id }.toHashSet()
|
||||||
|
val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds }
|
||||||
|
// SY -->
|
||||||
|
val trackingCount = supportedTrackerTracks.count {
|
||||||
|
(it.trackerId == TrackerManager.MDLIST && it.status != FollowStatus.UNFOLLOWED.long) ||
|
||||||
|
it.trackerId != TrackerManager.MDLIST
|
||||||
}
|
}
|
||||||
|
trackingCount to supportedTrackers.isNotEmpty()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collectLatest { trackItems ->
|
.collectLatest { (trackingCount, hasLoggedInTrackers) ->
|
||||||
updateSuccessState { it.copy(trackItems = trackItems) }
|
updateSuccessState {
|
||||||
|
it.copy(
|
||||||
|
trackingCount = trackingCount,
|
||||||
|
hasLoggedInTrackers = hasLoggedInTrackers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private suspend fun createMdListTrack(): TrackItem {
|
private suspend fun createMdListTrack(): Track {
|
||||||
val state = successState!!
|
val state = successState!!
|
||||||
val mdManga = state.manga.takeIf { it.source in mangaDexSourceIds }
|
val mdManga = state.manga.takeIf { it.source in mangaDexSourceIds }
|
||||||
?: state.mergedData?.manga?.values?.find { it.source in mangaDexSourceIds }
|
?: state.mergedData?.manga?.values?.find { it.source in mangaDexSourceIds }
|
||||||
@@ -1536,7 +1546,7 @@ class MangaScreenModel(
|
|||||||
val track = trackerManager.mdList.createInitialTracker(state.manga, mdManga)
|
val track = trackerManager.mdList.createInitialTracker(state.manga, mdManga)
|
||||||
.toDomainTrack(false)!!
|
.toDomainTrack(false)!!
|
||||||
insertTrack.await(track)
|
insertTrack.await(track)
|
||||||
return TrackItem(getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, trackerManager.mdList)
|
return getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -1633,7 +1643,8 @@ class MangaScreenModel(
|
|||||||
val chapters: List<ChapterList.Item>,
|
val chapters: List<ChapterList.Item>,
|
||||||
val availableScanlators: ImmutableSet<String>,
|
val availableScanlators: ImmutableSet<String>,
|
||||||
val excludedScanlators: ImmutableSet<String>,
|
val excludedScanlators: ImmutableSet<String>,
|
||||||
val trackItems: List<TrackItem> = emptyList(),
|
val trackingCount: Int = 0,
|
||||||
|
val hasLoggedInTrackers: Boolean = false,
|
||||||
val isRefreshingData: Boolean = false,
|
val isRefreshingData: Boolean = false,
|
||||||
val dialog: MangaScreenModel.Dialog? = null,
|
val dialog: MangaScreenModel.Dialog? = null,
|
||||||
val hasPromptedToAddBefore: Boolean = false,
|
val hasPromptedToAddBefore: Boolean = false,
|
||||||
@@ -1689,11 +1700,6 @@ class MangaScreenModel(
|
|||||||
val filterActive: Boolean
|
val filterActive: Boolean
|
||||||
get() = scanlatorFilterActive || manga.chaptersFiltered()
|
get() = scanlatorFilterActive || manga.chaptersFiltered()
|
||||||
|
|
||||||
val trackingCount: Int
|
|
||||||
get() = trackItems.count {
|
|
||||||
it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.long) || it.tracker !is MdList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the view filters to the list of chapters obtained from the database.
|
* Applies the view filters to the list of chapters obtained from the database.
|
||||||
* @return an observable of the list of chapters filtered and sorted.
|
* @return an observable of the list of chapters filtered and sorted.
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -170,6 +171,7 @@ data class TrackInfoDialogHomeScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onCopyLink = { context.copyTrackerLink(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +185,13 @@ data class TrackInfoDialogHomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Context.copyTrackerLink(trackItem: TrackItem) {
|
||||||
|
val url = trackItem.track?.remoteUrl ?: return
|
||||||
|
if (url.isNotBlank()) {
|
||||||
|
copyToClipboard(url, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class Model(
|
private class Model(
|
||||||
private val mangaId: Long,
|
private val mangaId: Long,
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
@@ -239,7 +248,7 @@ data class TrackInfoDialogHomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Track>.mapToTrackItem(): List<TrackItem> {
|
private fun List<Track>.mapToTrackItem(): List<TrackItem> {
|
||||||
val loggedInTrackers = Injekt.get<TrackerManager>().trackers.filter { it.isLoggedIn }
|
val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
|
||||||
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
|
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
|
||||||
return loggedInTrackers
|
return loggedInTrackers
|
||||||
// Map to TrackItem
|
// Map to TrackItem
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.reader
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistContent
|
import android.app.assist.AssistContent
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
@@ -35,6 +37,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.transition.doOnEnd
|
import androidx.core.transition.doOnEnd
|
||||||
@@ -269,6 +272,9 @@ class ReaderActivity : BaseActivity() {
|
|||||||
is ReaderViewModel.Event.ShareImage -> {
|
is ReaderViewModel.Event.ShareImage -> {
|
||||||
onShareImageResult(event.uri, event.page /* SY --> */, event.secondPage /* SY <-- */)
|
onShareImageResult(event.uri, event.page /* SY --> */, event.secondPage /* SY <-- */)
|
||||||
}
|
}
|
||||||
|
is ReaderViewModel.Event.CopyImage -> {
|
||||||
|
onCopyImageResult(event.uri)
|
||||||
|
}
|
||||||
is ReaderViewModel.Event.SetCoverResult -> {
|
is ReaderViewModel.Event.SetCoverResult -> {
|
||||||
onSetAsCoverResult(event.result)
|
onSetAsCoverResult(event.result)
|
||||||
}
|
}
|
||||||
@@ -1100,6 +1106,12 @@ class ReaderActivity : BaseActivity() {
|
|||||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onCopyImageResult(uri: Uri) {
|
||||||
|
val clipboardManager = applicationContext.getSystemService<ClipboardManager>() ?: return
|
||||||
|
val clipData = ClipData.newUri(applicationContext.contentResolver, "", uri)
|
||||||
|
clipboardManager.setPrimaryClip(clipData)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the presenter when a page is saved or fails. It shows a message or logs the
|
* Called from the presenter when a page is saved or fails. It shows a message or logs the
|
||||||
* event depending on the [result].
|
* event depending on the [result].
|
||||||
|
|||||||
@@ -1156,7 +1156,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
* get a path to the file and it has to be decompressed somewhere first. Only the last shared
|
* get a path to the file and it has to be decompressed somewhere first. Only the last shared
|
||||||
* image will be kept so it won't be taking lots of internal disk space.
|
* image will be kept so it won't be taking lots of internal disk space.
|
||||||
*/
|
*/
|
||||||
fun shareImage(useExtraPage: Boolean) {
|
fun shareImage(copyToClipboard: Boolean, useExtraPage: Boolean) {
|
||||||
// SY -->
|
// SY -->
|
||||||
val page = if (useExtraPage) {
|
val page = if (useExtraPage) {
|
||||||
(state.value.dialog as? Dialog.PageActions)?.extraPage
|
(state.value.dialog as? Dialog.PageActions)?.extraPage
|
||||||
@@ -1182,7 +1182,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
location = Location.Cache,
|
location = Location.Cache,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
eventChannel.send(Event.ShareImage(uri, page))
|
eventChannel.send(if (copyToClipboard) Event.CopyImage(uri) else Event.ShareImage(uri, page))
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
@@ -1190,7 +1190,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun shareImages() {
|
fun shareImages(copyToClipboard: Boolean) {
|
||||||
val (firstPage, secondPage) = (state.value.dialog as? Dialog.PageActions ?: return)
|
val (firstPage, secondPage) = (state.value.dialog as? Dialog.PageActions ?: return)
|
||||||
val viewer = state.value.viewer as? PagerViewer ?: return
|
val viewer = state.value.viewer as? PagerViewer ?: return
|
||||||
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
||||||
@@ -1214,7 +1214,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
location = Location.Cache,
|
location = Location.Cache,
|
||||||
manga = manga,
|
manga = manga,
|
||||||
)
|
)
|
||||||
eventChannel.send(Event.ShareImage(uri, firstPage, secondPage))
|
eventChannel.send(if (copyToClipboard) Event.CopyImage(uri) else Event.ShareImage(uri, firstPage, secondPage))
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
@@ -1382,5 +1382,6 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val page: ReaderPage/* SY --> */,
|
val page: ReaderPage/* SY --> */,
|
||||||
val secondPage: ReaderPage? = null, /* SY <-- */
|
val secondPage: ReaderPage? = null, /* SY <-- */
|
||||||
) : Event
|
) : Event
|
||||||
|
data class CopyImage(val uri: Uri) : Event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ class ReaderPreferences(
|
|||||||
|
|
||||||
fun flashOnPageChange() = preferenceStore.getBoolean("pref_reader_flash", false)
|
fun flashOnPageChange() = preferenceStore.getBoolean("pref_reader_flash", false)
|
||||||
|
|
||||||
|
fun flashDurationMillis() = preferenceStore.getInt("pref_reader_flash_duration", MILLI_CONVERSION)
|
||||||
|
|
||||||
|
fun flashPageInterval() = preferenceStore.getInt("pref_reader_flash_interval", 1)
|
||||||
|
|
||||||
|
fun flashColor() = preferenceStore.getEnum("pref_reader_flash_mode", FlashColor.BLACK)
|
||||||
|
|
||||||
fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500)
|
fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500)
|
||||||
|
|
||||||
fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
||||||
@@ -182,6 +188,12 @@ class ReaderPreferences(
|
|||||||
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
|
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
enum class FlashColor {
|
||||||
|
BLACK,
|
||||||
|
WHITE,
|
||||||
|
WHITE_BLACK
|
||||||
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(
|
enum class TappingInvertMode(
|
||||||
val titleRes: StringResource,
|
val titleRes: StringResource,
|
||||||
val shouldInvertHorizontal: Boolean = false,
|
val shouldInvertHorizontal: Boolean = false,
|
||||||
@@ -210,6 +222,8 @@ class ReaderPreferences(
|
|||||||
const val WEBTOON_PADDING_MIN = 0
|
const val WEBTOON_PADDING_MIN = 0
|
||||||
const val WEBTOON_PADDING_MAX = 25
|
const val WEBTOON_PADDING_MAX = 25
|
||||||
|
|
||||||
|
const val MILLI_CONVERSION = 100
|
||||||
|
|
||||||
val TapZones = listOf(
|
val TapZones = listOf(
|
||||||
MR.strings.label_default,
|
MR.strings.label_default,
|
||||||
MR.strings.l_nav,
|
MR.strings.l_nav,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.appcompat.widget.AppCompatImageView
|
|||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import coil3.BitmapImage
|
import coil3.BitmapImage
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.dispose
|
import coil3.dispose
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||||
@@ -244,16 +245,7 @@ class PagerPageHolder(
|
|||||||
private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource {
|
private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource {
|
||||||
// Handle adding a center margin to wide images if requested
|
// Handle adding a center margin to wide images if requested
|
||||||
if (imageSource2 == null) {
|
if (imageSource2 == null) {
|
||||||
return if (
|
return handleWideImage(imageSource)
|
||||||
!ImageUtil.isAnimatedAndSupported(imageSource) &&
|
|
||||||
ImageUtil.isWideImage(imageSource) &&
|
|
||||||
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
|
||||||
!viewer.config.imageCropBorders
|
|
||||||
) {
|
|
||||||
ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
|
|
||||||
} else {
|
|
||||||
imageSource
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.fullPage) return imageSource
|
if (page.fullPage) return imageSource
|
||||||
@@ -268,12 +260,7 @@ class PagerPageHolder(
|
|||||||
return imageSource
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageBitmap = try {
|
val imageBitmap = decodeImage(imageSource)
|
||||||
ImageDecoder.newInstance(imageSource.inputStream())?.decode()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (imageBitmap == null) {
|
if (imageBitmap == null) {
|
||||||
imageSource2.close()
|
imageSource2.close()
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
@@ -281,23 +268,16 @@ class PagerPageHolder(
|
|||||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageSource
|
return imageSource
|
||||||
}
|
}
|
||||||
scope.launch { progressIndicator.setProgress(96) }
|
|
||||||
val height = imageBitmap.height
|
|
||||||
val width = imageBitmap.width
|
|
||||||
|
|
||||||
if (height < width) {
|
scope.launch { progressIndicator.setProgress(96) }
|
||||||
|
if (imageBitmap.height < imageBitmap.width) {
|
||||||
imageSource2.close()
|
imageSource2.close()
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageSource
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageBitmap2 = try {
|
val imageBitmap2 = decodeImage(imageSource2)
|
||||||
ImageDecoder.newInstance(imageSource2.inputStream())?.decode()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (imageBitmap2 == null) {
|
if (imageBitmap2 == null) {
|
||||||
imageSource2.close()
|
imageSource2.close()
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
@@ -306,35 +286,63 @@ class PagerPageHolder(
|
|||||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageSource
|
return imageSource
|
||||||
}
|
}
|
||||||
scope.launch { progressIndicator.setProgress(97) }
|
|
||||||
val height2 = imageBitmap2.height
|
|
||||||
val width2 = imageBitmap2.width
|
|
||||||
|
|
||||||
if (height2 < width2) {
|
scope.launch { progressIndicator.setProgress(97) }
|
||||||
|
if (imageBitmap2.height < imageBitmap2.width) {
|
||||||
imageSource2.close()
|
imageSource2.close()
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageSource
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
|
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
|
||||||
|
val centerMargin = calculateCenterMargin(imageBitmap.height, imageBitmap2.height)
|
||||||
|
|
||||||
imageSource.close()
|
imageSource.close()
|
||||||
imageSource2.close()
|
imageSource2.close()
|
||||||
|
|
||||||
val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
|
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, centerMargin, viewer.config.pageCanvasColor) {
|
||||||
|
updateProgress(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleWideImage(imageSource: BufferedSource): BufferedSource {
|
||||||
|
return if (
|
||||||
|
!ImageUtil.isAnimatedAndSupported(imageSource) &&
|
||||||
|
ImageUtil.isWideImage(imageSource) &&
|
||||||
|
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
||||||
|
!viewer.config.imageCropBorders
|
||||||
|
) {
|
||||||
|
ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
|
||||||
|
} else {
|
||||||
|
imageSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeImage(imageSource: BufferedSource): Bitmap? {
|
||||||
|
return try {
|
||||||
|
ImageDecoder.newInstance(imageSource.inputStream())?.decode()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Cannot decode image" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateCenterMargin(height: Int, height2: Int): Int {
|
||||||
|
return if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
|
||||||
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
|
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, centerMargin, viewer.config.pageCanvasColor) {
|
private fun updateProgress(progress: Int) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (it == 100) {
|
if (progress == 100) {
|
||||||
progressIndicator.hide()
|
progressIndicator.hide()
|
||||||
} else {
|
} else {
|
||||||
progressIndicator.setProgress(it)
|
progressIndicator.setProgress(progress)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-74
@@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.common.util.lang.launchUI
|
import tachiyomi.core.common.util.lang.launchUI
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||||
@@ -231,10 +230,12 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
||||||
if (!viewer.config.doublePages) {
|
if (!viewer.config.doublePages) {
|
||||||
// If not in double mode, set up items like before
|
// If not in double mode, set up items like before
|
||||||
subItems.forEach {
|
subItems.forEach { readerItem ->
|
||||||
(it as? ReaderPage)?.shiftedPage = false
|
if (readerItem is ReaderPage) {
|
||||||
|
readerItem.shiftedPage = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.joinedItems = subItems.map { Pair<ReaderItem, ReaderItem?>(it, null) }.toMutableList()
|
this.joinedItems = subItems.map { Pair(it, null) }.toMutableList()
|
||||||
if (viewer is R2LPagerViewer) {
|
if (viewer is R2LPagerViewer) {
|
||||||
joinedItems.reverse()
|
joinedItems.reverse()
|
||||||
}
|
}
|
||||||
@@ -242,54 +243,43 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
val pagedItems = mutableListOf<MutableList<ReaderPage?>>()
|
val pagedItems = mutableListOf<MutableList<ReaderPage?>>()
|
||||||
val otherItems = mutableListOf<ReaderItem>()
|
val otherItems = mutableListOf<ReaderItem>()
|
||||||
pagedItems.add(mutableListOf())
|
pagedItems.add(mutableListOf())
|
||||||
|
|
||||||
// Step 1: segment the pages and transition pages
|
// Step 1: segment the pages and transition pages
|
||||||
subItems.forEach {
|
subItems.forEach { readerItem ->
|
||||||
when (it) {
|
when (readerItem) {
|
||||||
is ReaderPage -> {
|
is ReaderPage -> {
|
||||||
if (pagedItems.last().lastOrNull() != null &&
|
if (pagedItems.last().isNotEmpty() && pagedItems.last().last()?.chapter?.chapter?.id != readerItem.chapter.chapter.id) {
|
||||||
pagedItems.last().last()?.chapter?.chapter?.id != it.chapter.chapter.id
|
|
||||||
) {
|
|
||||||
pagedItems.add(mutableListOf())
|
pagedItems.add(mutableListOf())
|
||||||
}
|
}
|
||||||
pagedItems.last().add(it)
|
pagedItems.last().add(readerItem)
|
||||||
}
|
}
|
||||||
is ChapterTransition -> {
|
is ChapterTransition -> {
|
||||||
otherItems.add(it)
|
otherItems.add(readerItem)
|
||||||
pagedItems.add(mutableListOf())
|
pagedItems.add(mutableListOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var pagedIndex = 0
|
|
||||||
val subJoinedItems = mutableListOf<Pair<ReaderItem, ReaderItem?>>()
|
val subJoinedItems = mutableListOf<Pair<ReaderItem, ReaderItem?>>()
|
||||||
|
|
||||||
// Step 2: run through each set of pages
|
// Step 2: run through each set of pages
|
||||||
pagedItems.forEach { items ->
|
pagedItems.forEach { items ->
|
||||||
|
items.forEach { it?.shiftedPage = false }
|
||||||
|
|
||||||
items.forEach {
|
|
||||||
it?.shiftedPage = false
|
|
||||||
}
|
|
||||||
// Step 3: If pages have been shifted,
|
// Step 3: If pages have been shifted,
|
||||||
if (viewer.config.shiftDoublePage) {
|
if (viewer.config.shiftDoublePage) {
|
||||||
|
val index = items.indexOf(pageToShift)
|
||||||
|
// Go from the current page and work your way back to the first page,
|
||||||
|
// or the first page that's a full page.
|
||||||
|
// This is done in case user tries to shift a page after a full page
|
||||||
|
val fullPageBeforeIndex = if (index > -1) {
|
||||||
|
items.take(index).indexOfLast { it?.fullPage == true }
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}.coerceAtLeast(0)
|
||||||
|
|
||||||
|
// Add a shifted page to the first place there isnt a full page
|
||||||
run loop@{
|
run loop@{
|
||||||
var index = items.indexOf(pageToShift)
|
|
||||||
if (pageToShift?.fullPage == true) {
|
|
||||||
index = max(0, index - 1)
|
|
||||||
}
|
|
||||||
// Go from the current page and work your way back to the first page,
|
|
||||||
// or the first page that's a full page.
|
|
||||||
// This is done in case user tries to shift a page after a full page
|
|
||||||
val fullPageBeforeIndex = max(
|
|
||||||
0,
|
|
||||||
(
|
|
||||||
if (index > -1) {
|
|
||||||
(
|
|
||||||
items.take(index).indexOfLast { it?.fullPage == true }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Add a shifted page to the first place there isnt a full page
|
|
||||||
(fullPageBeforeIndex until items.size).forEach {
|
(fullPageBeforeIndex until items.size).forEach {
|
||||||
if (items[it]?.fullPage == false) {
|
if (items[it]?.fullPage == false) {
|
||||||
items[it]?.shiftedPage = true
|
items[it]?.shiftedPage = true
|
||||||
@@ -302,12 +292,15 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
// Step 4: Add blanks for chunking
|
// Step 4: Add blanks for chunking
|
||||||
var itemIndex = 0
|
var itemIndex = 0
|
||||||
while (itemIndex < items.size) {
|
while (itemIndex < items.size) {
|
||||||
items[itemIndex]?.isolatedPage = false
|
val currentItem = items[itemIndex]
|
||||||
if (items[itemIndex]?.fullPage == true || items[itemIndex]?.shiftedPage == true) {
|
currentItem?.isolatedPage = false
|
||||||
|
if (currentItem?.fullPage == true || currentItem?.shiftedPage == true) {
|
||||||
// Add a 'blank' page after each full page. It will be used when chunked to solo a page
|
// Add a 'blank' page after each full page. It will be used when chunked to solo a page
|
||||||
items.add(itemIndex + 1, null)
|
items.add(itemIndex + 1, null)
|
||||||
if (items[itemIndex]?.fullPage == true && itemIndex > 0 &&
|
if (
|
||||||
items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0
|
currentItem.fullPage && itemIndex > 0 &&
|
||||||
|
items[itemIndex - 1] != null &&
|
||||||
|
(itemIndex - 1) % 2 == 0
|
||||||
) {
|
) {
|
||||||
// If a page is a full page, check if the previous page needs to be isolated
|
// If a page is a full page, check if the previous page needs to be isolated
|
||||||
// we should check if it's an even or odd page, since even pages need shifting
|
// we should check if it's an even or odd page, since even pages need shifting
|
||||||
@@ -325,15 +318,14 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
|
|
||||||
// Step 5: chunk em
|
// Step 5: chunk em
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
subJoinedItems.addAll(
|
subJoinedItems.addAll(items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) })
|
||||||
items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
otherItems.getOrNull(pagedIndex)?.let {
|
|
||||||
|
otherItems.getOrNull(pagedItems.indexOf(items))?.let {
|
||||||
subJoinedItems.add(Pair(it, null))
|
subJoinedItems.add(Pair(it, null))
|
||||||
pagedIndex++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewer is R2LPagerViewer) {
|
if (viewer is R2LPagerViewer) {
|
||||||
subJoinedItems.reverse()
|
subJoinedItems.reverse()
|
||||||
}
|
}
|
||||||
@@ -347,42 +339,37 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
// we need to set the page back correctly
|
// we need to set the page back correctly
|
||||||
// We will however shift to the first page of the new chapter if the last page we were are
|
// We will however shift to the first page of the new chapter if the last page we were are
|
||||||
// on is not in the new chapter that has loaded
|
// on is not in the new chapter that has loaded
|
||||||
val newPage =
|
val newPage = when {
|
||||||
when {
|
oldCurrent?.first is ReaderPage && (oldCurrent.first as ReaderPage).chapter != currentChapter &&
|
||||||
(oldCurrent?.first as? ReaderPage)?.chapter != currentChapter &&
|
(oldCurrent.second as? ChapterTransition)?.from != currentChapter ->
|
||||||
(oldCurrent?.first as? ChapterTransition)?.from != currentChapter -> subItems.find {
|
subItems.find { it is ReaderPage && it.chapter == currentChapter }
|
||||||
(it as? ReaderPage)?.chapter == currentChapter
|
useSecondPage -> oldCurrent?.second ?: oldCurrent?.first
|
||||||
}
|
else -> oldCurrent?.first ?: return
|
||||||
useSecondPage -> (oldCurrent?.second ?: oldCurrent?.first)
|
|
||||||
else -> oldCurrent?.first ?: return
|
|
||||||
}
|
|
||||||
var index = joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
|
|
||||||
if (newPage is ChapterTransition && index == -1) {
|
|
||||||
val newerPage = if (newPage is ChapterTransition.Next) {
|
|
||||||
joinedItems.filter {
|
|
||||||
(it.first as? ReaderPage)?.chapter == newPage.to
|
|
||||||
}.minByOrNull { (it.first as? ReaderPage)?.index ?: Int.MAX_VALUE }?.first
|
|
||||||
} else {
|
|
||||||
joinedItems.filter {
|
|
||||||
(it.first as? ReaderPage)?.chapter == newPage.to
|
|
||||||
}.maxByOrNull { (it.first as? ReaderPage)?.index ?: Int.MIN_VALUE }?.first
|
|
||||||
}
|
|
||||||
index = joinedItems.indexOfFirst { it.first == newerPage || it.second == newerPage }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val index = when (newPage) {
|
||||||
|
is ChapterTransition -> {
|
||||||
|
val filteredPages = joinedItems.filter { it.first is ReaderPage && (it.first as ReaderPage).chapter == newPage.to }
|
||||||
|
val page = if (newPage is ChapterTransition.Next) {
|
||||||
|
filteredPages.minByOrNull { (it.first as ReaderPage).index }?.first
|
||||||
|
} else {
|
||||||
|
filteredPages.maxByOrNull { (it.first as ReaderPage).index }?.first
|
||||||
|
}
|
||||||
|
joinedItems.indexOfFirst { it.first == page || it.second == page }
|
||||||
|
}
|
||||||
|
else -> joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
|
||||||
|
}
|
||||||
|
|
||||||
viewer.pager.setCurrentItem(index, false)
|
viewer.pager.setCurrentItem(index, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun splitDoublePages(current: ReaderPage) {
|
fun splitDoublePages(current: ReaderPage) {
|
||||||
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
||||||
setJoinedItems(
|
val oldSecondPage = oldCurrent?.second as? ReaderPage
|
||||||
oldCurrent?.second == current ||
|
val oldFirstPage = oldCurrent?.first as? ReaderPage
|
||||||
(current.index + 1) < (
|
val oldPage = oldSecondPage ?: oldFirstPage
|
||||||
(
|
|
||||||
oldCurrent?.second
|
setJoinedItems(oldSecondPage == current || (current.index + 1) < (oldPage?.index ?: 0))
|
||||||
?: oldCurrent?.first
|
|
||||||
) as? ReaderPage
|
|
||||||
)?.index ?: 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The listener may be removed when we split a page, so the ui may not have updated properly
|
// The listener may be removed when we split a page, so the ui may not have updated properly
|
||||||
// This case usually happens when we load a new chapter and the first 2 pages need to split og
|
// This case usually happens when we load a new chapter and the first 2 pages need to split og
|
||||||
|
|||||||
+2
-6
@@ -13,12 +13,7 @@ import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
|||||||
* This layout manager uses the same package name as the support library in order to use a package
|
* This layout manager uses the same package name as the support library in order to use a package
|
||||||
* protected method.
|
* protected method.
|
||||||
*/
|
*/
|
||||||
class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) {
|
class WebtoonLayoutManager(context: Context, private val extraLayoutSpace: Int) : LinearLayoutManager(context) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra layout space is set to half the screen height.
|
|
||||||
*/
|
|
||||||
private val extraLayoutSpace = context.resources.displayMetrics.heightPixels / 2
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isItemPrefetchEnabled = false
|
isItemPrefetchEnabled = false
|
||||||
@@ -27,6 +22,7 @@ class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) {
|
|||||||
/**
|
/**
|
||||||
* Returns the custom extra layout space.
|
* Returns the custom extra layout space.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
|
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
|
||||||
return extraLayoutSpace
|
return extraLayoutSpace
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,15 @@ class WebtoonViewer(
|
|||||||
*/
|
*/
|
||||||
private val frame = WebtoonFrame(activity)
|
private val frame = WebtoonFrame(activity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distance to scroll when the user taps on one side of the recycler view.
|
||||||
|
*/
|
||||||
|
private val scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout manager of the recycler view.
|
* Layout manager of the recycler view.
|
||||||
*/
|
*/
|
||||||
private val layoutManager = WebtoonLayoutManager(activity)
|
private val layoutManager = WebtoonLayoutManager(activity, scrollDistance)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration used by this viewer, like allow taps, or crop image borders.
|
* Configuration used by this viewer, like allow taps, or crop image borders.
|
||||||
@@ -68,11 +73,6 @@ class WebtoonViewer(
|
|||||||
*/
|
*/
|
||||||
private val adapter = WebtoonAdapter(this)
|
private val adapter = WebtoonAdapter(this)
|
||||||
|
|
||||||
/**
|
|
||||||
* Distance to scroll when the user taps on one side of the recycler view.
|
|
||||||
*/
|
|
||||||
private var scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently active item. It can be a chapter page or a chapter transition.
|
* Currently active item. It can be a chapter page or a chapter transition.
|
||||||
*/
|
*/
|
||||||
@@ -86,6 +86,7 @@ class WebtoonViewer(
|
|||||||
.threshold
|
.threshold
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
recycler.setItemViewCacheSize(RecyclerViewCacheSize)
|
||||||
recycler.isVisible = false // Don't let the recycler layout yet
|
recycler.isVisible = false // Don't let the recycler layout yet
|
||||||
recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
recycler.isFocusable = false
|
recycler.isFocusable = false
|
||||||
@@ -400,3 +401,5 @@ class WebtoonViewer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val RecyclerViewCacheSize = 4
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class StatsScreenModel(
|
|||||||
// SY <--
|
// SY <--
|
||||||
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
|
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
|
||||||
|
|
||||||
private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } }
|
private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val _allRead = MutableStateFlow(false)
|
private val _allRead = MutableStateFlow(false)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
|
|||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -47,7 +47,9 @@ class WebViewScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clearCookies(url: String) {
|
fun clearCookies(url: String) {
|
||||||
val cleared = network.cookieJar.remove(url.toHttpUrl())
|
url.toHttpUrlOrNull()?.let {
|
||||||
logcat { "Cleared $cleared cookies for: $url" }
|
val cleared = network.cookieJar.remove(it)
|
||||||
|
logcat { "Cleared $cleared cookies for: $url" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import coil3.gif.ScaleDrawable
|
import coil3.size.ScaleDrawable
|
||||||
|
|
||||||
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
|
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
|
||||||
is BitmapDrawable -> bitmap
|
is BitmapDrawable -> bitmap
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.track.Tracker
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -16,6 +18,7 @@ data class DummyTracker(
|
|||||||
override val name: String,
|
override val name: String,
|
||||||
override val supportsReadingDates: Boolean = false,
|
override val supportsReadingDates: Boolean = false,
|
||||||
override val isLoggedIn: Boolean = false,
|
override val isLoggedIn: Boolean = false,
|
||||||
|
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
||||||
val valLogoColor: Int = Color.rgb(18, 25, 35),
|
val valLogoColor: Int = Color.rgb(18, 25, 35),
|
||||||
val valLogo: Int = R.drawable.ic_tracker_anilist,
|
val valLogo: Int = R.drawable.ic_tracker_anilist,
|
||||||
val valStatuses: List<Long> = (1L..6L).toList(),
|
val valStatuses: List<Long> = (1L..6L).toList(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
|
|||||||
ENGLISH("en"),
|
ENGLISH("en"),
|
||||||
JAPANESE("ja"),
|
JAPANESE("ja"),
|
||||||
POLISH("pl"),
|
POLISH("pl"),
|
||||||
SERBO_CROATIAN("rs", "sh"),
|
SERBIAN("sh"),
|
||||||
DUTCH("nl"),
|
DUTCH("nl"),
|
||||||
ITALIAN("it"),
|
ITALIAN("it"),
|
||||||
RUSSIAN("ru"),
|
RUSSIAN("ru"),
|
||||||
@@ -29,7 +29,7 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
|
|||||||
MONGOLIAN("mn"),
|
MONGOLIAN("mn"),
|
||||||
TURKISH("tr"),
|
TURKISH("tr"),
|
||||||
INDONESIAN("id"),
|
INDONESIAN("id"),
|
||||||
KOREAN("kr", "ko"),
|
KOREAN("ko"),
|
||||||
SPANISH_LATAM("es-la", "es-419"),
|
SPANISH_LATAM("es-la", "es-419"),
|
||||||
PERSIAN("fa"),
|
PERSIAN("fa"),
|
||||||
MALAY("ms"),
|
MALAY("ms"),
|
||||||
@@ -51,12 +51,12 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromIsoCode(isoCode: String): MdLang? =
|
fun fromIsoCode(isoCode: String): MdLang? =
|
||||||
values().firstOrNull {
|
entries.firstOrNull {
|
||||||
it.lang == isoCode
|
it.lang == isoCode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromExt(extLang: String): MdLang? =
|
fun fromExt(extLang: String): MdLang? =
|
||||||
values().firstOrNull {
|
entries.firstOrNull {
|
||||||
it.extLang == extLang
|
it.extLang == extLang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ private fun isHentaiTag(tag: String): Boolean {
|
|||||||
tag.contains("nsfw", true) ||
|
tag.contains("nsfw", true) ||
|
||||||
tag.contains("erotica", true) ||
|
tag.contains("erotica", true) ||
|
||||||
tag.contains("pornographic", true) ||
|
tag.contains("pornographic", true) ||
|
||||||
|
tag.contains("mature", true) ||
|
||||||
tag.contains("18+", true)
|
tag.contains("18+", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,8 @@ private fun isHentaiSource(source: String): Boolean {
|
|||||||
source.contains("hbrowse", true) ||
|
source.contains("hbrowse", true) ||
|
||||||
source.contains("nhentai", true) ||
|
source.contains("nhentai", true) ||
|
||||||
source.contains("erofus", true) ||
|
source.contains("erofus", true) ||
|
||||||
|
source.contains("luscious", true) ||
|
||||||
|
source.contains("doujins", true) ||
|
||||||
source.contains("multporn", true) ||
|
source.contains("multporn", true) ||
|
||||||
source.contains("vcp", true) ||
|
source.contains("vcp", true) ||
|
||||||
source.contains("vmp", true) ||
|
source.contains("vmp", true) ||
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ class MigrationStrategyFactory(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(old: Int, new: Int): MigrationStrategy {
|
fun create(old: Int, new: Int): MigrationStrategy {
|
||||||
val versions = (old + 1)..new
|
|
||||||
val strategy = when {
|
val strategy = when {
|
||||||
old == 0 -> InitialMigrationStrategy(
|
old == 0 -> InitialMigrationStrategy(
|
||||||
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
|
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
|
||||||
)
|
)
|
||||||
|
|
||||||
old >= new -> NoopMigrationStrategy(false)
|
old >= new -> NoopMigrationStrategy(false)
|
||||||
else -> VersionRangeMigrationStrategy(
|
else -> VersionRangeMigrationStrategy(
|
||||||
versions = versions,
|
versions = (old + 1)..new,
|
||||||
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
|
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
object Migrator {
|
object Migrator {
|
||||||
|
|
||||||
private var result: Deferred<Boolean>? = null
|
private var result: Deferred<Boolean>? = null
|
||||||
val scope = CoroutineScope(Dispatchers.Main + Job())
|
val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||||
|
|
||||||
fun initialize(
|
fun initialize(
|
||||||
old: Int,
|
old: Int,
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -12,7 +12,7 @@ class ChangeThemeModeToUppercaseMigration : Migration {
|
|||||||
override val version: Float = 42f
|
override val version: Float = 42f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val uiPreferences = migrationContext.get<UiPreferences>() ?: return@withIOContext false
|
val uiPreferences = migrationContext.get<UiPreferences>() ?: return@withIOContext false
|
||||||
if (uiPreferences.themeMode().isSet()) {
|
if (uiPreferences.themeMode().isSet()) {
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -11,7 +11,7 @@ class ChangeTrackingQueueTypeMigration : Migration {
|
|||||||
override val version: Float = 44f
|
override val version: Float = 44f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||||
trackingQueuePref.all.forEach {
|
trackingQueuePref.all.forEach {
|
||||||
val (_, lastChapterRead) = it.value.toString().split(":")
|
val (_, lastChapterRead) = it.value.toString().split(":")
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.App
|
import android.app.Application
|
||||||
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
@@ -13,7 +13,7 @@ class ClearBrokenPagePreviewCacheMigration : Migration {
|
|||||||
override val version: Float = 58f
|
override val version: Float = 58f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
val pagePreviewCache = migrationContext.get<PagePreviewCache>() ?: return@withIOContext false
|
val pagePreviewCache = migrationContext.get<PagePreviewCache>() ?: return@withIOContext false
|
||||||
pagePreviewCache.clear()
|
pagePreviewCache.clear()
|
||||||
File(context.cacheDir, PagePreviewCache.PARAMETER_CACHE_DIRECTORY).listFiles()?.forEach {
|
File(context.cacheDir, PagePreviewCache.PARAMETER_CACHE_DIRECTORY).listFiles()?.forEach {
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.App
|
import android.app.Application
|
||||||
import exh.log.xLogE
|
import exh.log.xLogE
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
@@ -11,7 +11,7 @@ class DeleteOldEhFavoritesDatabaseMigration : Migration {
|
|||||||
override val version: Float = 24f
|
override val version: Float = 24f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
try {
|
try {
|
||||||
sequenceOf(
|
sequenceOf(
|
||||||
"fav-sync",
|
"fav-sync",
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
@@ -11,7 +11,7 @@ class MoveCacheToDiskSettingMigration : Migration {
|
|||||||
override val version: Float = 66f
|
override val version: Float = 66f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
|
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
|
||||||
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
|
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package mihon.core.migration.migrations
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -11,7 +11,7 @@ class MoveCatalogueCoverOnlyGridSettingMigration : Migration {
|
|||||||
override val version: Float = 29f
|
override val version: Float = 29f
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val context = migrationContext.get<App>() ?: return@withIOContext false
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
if (prefs.getString("pref_display_mode_catalogue", null) == "NO_TITLE_GRID") {
|
if (prefs.getString("pref_display_mode_catalogue", null) == "NO_TITLE_GRID") {
|
||||||
prefs.edit(commit = true) {
|
prefs.edit(commit = true) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user