mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
chore: merge upstream.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
commit
ef6da09b7e
@ -3,5 +3,5 @@ indent_size=4
|
|||||||
insert_final_newline=true
|
insert_final_newline=true
|
||||||
ij_kotlin_allow_trailing_comma=true
|
ij_kotlin_allow_trailing_comma=true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
ij_kotlin_name_count_to_use_star_import=2147483647
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
ij_kotlin_name_count_to_use_star_import_for_members=2147483647
|
||||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -5,7 +5,7 @@ I acknowledge that:
|
|||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.14.6)
|
- To the latest version of the app (stable is v0.14.6)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have gone through the FAQ (https://tachiyomi.org/help/faq/) and troubleshooting guide (https://tachiyomi.org/help/guides/troubleshooting/)
|
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||||
- I will fill out the title and the information in this template
|
- I will fill out the title and the information in this template
|
||||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -4,8 +4,8 @@ contact_links:
|
|||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
- name: 📦 Tachiyomi extensions
|
- name: 📦 Tachiyomi extensions
|
||||||
url: https://tachiyomi.org/extensions
|
url: https://tachiyomi.org/extensions/
|
||||||
about: List of all available extensions with download links
|
about: List of all available extensions with download links
|
||||||
- name: 🖥️ Tachiyomi website
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
|
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -96,7 +96,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://tachiyomi.org/help/faq/) and [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
@ -36,4 +36,4 @@ jobs:
|
|||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
14
.github/workflows/build_push.yml
vendored
14
.github/workflows/build_push.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
@ -104,3 +104,13 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
update-website:
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
steps:
|
||||||
|
- name: Trigger Netlify build hook
|
||||||
|
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
|
||||||
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"labels": ["Cloudflare protected"],
|
"labels": ["Cloudflare protected"],
|
||||||
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
auto-close-ignore-label: do-not-autoclose
|
auto-close-ignore-label: do-not-autoclose
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,7 +2,8 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/*
|
||||||
|
!.idea/icon.png
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
BIN
.idea/icon.png
Normal file
BIN
.idea/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -30,7 +30,7 @@ Before you start, please note that the ability to use following technologies is
|
|||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.
|
||||||
|
|
||||||
|
|
||||||
# Forks
|
# Forks
|
||||||
|
@ -29,7 +29,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Issues</summary>
|
<details><summary>Issues</summary>
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
|
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@ -23,7 +22,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
|
||||||
versionCode = 105
|
versionCode = 107
|
||||||
versionName = "0.14.6"
|
versionName = "0.14.6"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
@ -104,7 +103,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
resources.excludes.addAll(listOf(
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
"META-INF/DEPENDENCIES",
|
"META-INF/DEPENDENCIES",
|
||||||
"LICENSE.txt",
|
"LICENSE.txt",
|
||||||
"META-INF/LICENSE",
|
"META-INF/LICENSE",
|
||||||
@ -112,7 +112,8 @@ android {
|
|||||||
"META-INF/README.md",
|
"META-INF/README.md",
|
||||||
"META-INF/NOTICE",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/*.kotlin_module",
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@ -239,7 +240,6 @@ dependencies {
|
|||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
implementation(libs.compose.simpleicons)
|
|
||||||
implementation(libs.swipe)
|
implementation(libs.swipe)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
@ -267,7 +267,9 @@ androidComponents {
|
|||||||
beforeVariants { variantBuilder ->
|
beforeVariants { variantBuilder ->
|
||||||
// Disables standardBenchmark
|
// Disables standardBenchmark
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
if (variantBuilder.buildType == "benchmark") {
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
||||||
|
listOf("default" to "dev"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
@ -278,10 +280,6 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
withType<LintTask>().configureEach {
|
|
||||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
@ -306,12 +304,12 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.buildDir.absolutePath + "/compose_metrics",
|
||||||
)
|
)
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.buildDir.absolutePath + "/compose_metrics",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,20 +155,6 @@
|
|||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
|
|
||||||
android:enabled="@bool/glance_appwidget_available"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/label_recent_updates">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/updates_grid_glance_widget_info" />
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.domain
|
package eu.kanade.domain
|
||||||
|
|
||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||||
@ -16,7 +15,9 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
|
|||||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||||
import eu.kanade.domain.source.interactor.ToggleSource
|
import eu.kanade.domain.source.interactor.ToggleSource
|
||||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||||
|
import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||||
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
@ -50,6 +51,7 @@ import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
|||||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||||
import tachiyomi.domain.history.repository.HistoryRepository
|
import tachiyomi.domain.history.repository.HistoryRepository
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
@ -57,7 +59,6 @@ import tachiyomi.domain.manga.interactor.GetManga
|
|||||||
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
@ -102,7 +103,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
addFactory { SetFetchInterval(get()) }
|
addFactory { FetchInterval(get()) }
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
@ -114,11 +115,13 @@ class DomainModule : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||||
addFactory { TrackChapter(get(), get(), get(), get()) }
|
addFactory { TrackChapter(get(), get(), get(), get()) }
|
||||||
|
addFactory { AddTracks(get(), get(), get()) }
|
||||||
addFactory { RefreshTracks(get(), get(), get(), get()) }
|
addFactory { RefreshTracks(get(), get(), get(), get()) }
|
||||||
addFactory { DeleteTrack(get()) }
|
addFactory { DeleteTrack(get()) }
|
||||||
addFactory { GetTracksPerManga(get()) }
|
addFactory { GetTracksPerManga(get()) }
|
||||||
addFactory { GetTracks(get()) }
|
addFactory { GetTracks(get()) }
|
||||||
addFactory { InsertTrack(get()) }
|
addFactory { InsertTrack(get()) }
|
||||||
|
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
addFactory { GetChapter(get()) }
|
addFactory { GetChapter(get()) }
|
||||||
@ -127,7 +130,6 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
addFactory { ShouldUpdateDbChapter() }
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
|
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
addFactory { GetHistory(get()) }
|
addFactory { GetHistory(get()) }
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.domain.manga.interactor
|
|||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
@ -15,7 +15,7 @@ import java.util.Date
|
|||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
private val setFetchInterval: SetFetchInterval,
|
private val fetchInterval: FetchInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
@ -79,9 +79,9 @@ class UpdateManga(
|
|||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = setFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
?.let { mangaRepository.update(it) }
|
?.let { mangaRepository.update(it) }
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
|
||||||
|
class AddTracks(
|
||||||
|
private val getTracks: GetTracks,
|
||||||
|
private val insertTrack: InsertTrack,
|
||||||
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun bindEnhancedTracks(manga: Manga, source: Source) = withNonCancellableContext {
|
||||||
|
getTracks.await(manga.id)
|
||||||
|
.filterIsInstance<EnhancedTracker>()
|
||||||
|
.filter { it.accept(source) }
|
||||||
|
.forEach { service ->
|
||||||
|
try {
|
||||||
|
service.match(manga)?.let { track ->
|
||||||
|
track.manga_id = manga.id
|
||||||
|
(service as Tracker).bind(track)
|
||||||
|
insertTrack.await(track.toDomainTrack()!!)
|
||||||
|
|
||||||
|
syncChapterProgressWithTrack.await(
|
||||||
|
manga.id,
|
||||||
|
track.toDomainTrack()!!,
|
||||||
|
service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(
|
||||||
|
LogPriority.WARN,
|
||||||
|
e,
|
||||||
|
) { "Could not match manga: ${manga.title} with service $service" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
package eu.kanade.domain.track.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
|
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
@ -13,7 +12,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
|
|||||||
|
|
||||||
class RefreshTracks(
|
class RefreshTracks(
|
||||||
private val getTracks: GetTracks,
|
private val getTracks: GetTracks,
|
||||||
private val trackManager: TrackManager,
|
private val trackerManager: TrackerManager,
|
||||||
private val insertTrack: InsertTrack,
|
private val insertTrack: InsertTrack,
|
||||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
) {
|
) {
|
||||||
@ -23,18 +22,17 @@ class RefreshTracks(
|
|||||||
*
|
*
|
||||||
* @return Failed updates.
|
* @return Failed updates.
|
||||||
*/
|
*/
|
||||||
suspend fun await(mangaId: Long): List<Pair<TrackService?, Throwable>> {
|
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
return@supervisorScope getTracks.await(mangaId)
|
return@supervisorScope getTracks.await(mangaId)
|
||||||
.map { track ->
|
.map { it to trackerManager.get(it.syncId) }
|
||||||
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
|
.map { (track, service) ->
|
||||||
async {
|
async {
|
||||||
val service = trackManager.getService(track.syncId)
|
|
||||||
return@async try {
|
return@async try {
|
||||||
if (service?.isLoggedIn == true) {
|
val updatedTrack = service!!.refresh(track.toDbTrack())
|
||||||
val updatedTrack = service.refresh(track.toDbTrack())
|
|
||||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
||||||
syncChapterProgressWithTrack.await(mangaId, track, service)
|
syncChapterProgressWithTrack.await(mangaId, track, service)
|
||||||
}
|
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
service to e
|
service to e
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
||||||
@ -20,9 +20,9 @@ class SyncChapterProgressWithTrack(
|
|||||||
suspend fun await(
|
suspend fun await(
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
remoteTrack: Track,
|
remoteTrack: Track,
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
) {
|
) {
|
||||||
if (service !is EnhancedTrackService) {
|
if (tracker !is EnhancedTracker) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class SyncChapterProgressWithTrack(
|
|||||||
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
service.update(updatedTrack.toDbTrack())
|
tracker.update(updatedTrack.toDbTrack())
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
insertTrack.await(updatedTrack)
|
insertTrack.await(updatedTrack)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
@ -4,30 +4,29 @@ import android.content.Context
|
|||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
|
||||||
class TrackChapter(
|
class TrackChapter(
|
||||||
private val getTracks: GetTracks,
|
private val getTracks: GetTracks,
|
||||||
private val trackManager: TrackManager,
|
private val trackerManager: TrackerManager,
|
||||||
private val insertTrack: InsertTrack,
|
private val insertTrack: InsertTrack,
|
||||||
private val delayedTrackingStore: DelayedTrackingStore,
|
private val delayedTrackingStore: DelayedTrackingStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope {
|
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
|
||||||
launchNonCancellable {
|
withNonCancellableContext {
|
||||||
val tracks = getTracks.await(mangaId)
|
val tracks = getTracks.await(mangaId)
|
||||||
if (tracks.isEmpty()) return@launchNonCancellable
|
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||||
|
|
||||||
tracks.mapNotNull { track ->
|
tracks.mapNotNull { track ->
|
||||||
val service = trackManager.getService(track.syncId)
|
val service = trackerManager.get(track.syncId)
|
||||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
@ -33,7 +33,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
val getTracks = Injekt.get<GetTracks>()
|
val getTracks = Injekt.get<GetTracks>()
|
||||||
val insertTrack = Injekt.get<InsertTrack>()
|
val insertTrack = Injekt.get<InsertTrack>()
|
||||||
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackerManager = Injekt.get<TrackerManager>()
|
||||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
}
|
}
|
||||||
.forEach { track ->
|
.forEach { track ->
|
||||||
try {
|
try {
|
||||||
val service = trackManager.getService(track.syncId)
|
val service = trackerManager.get(track.syncId)
|
||||||
if (service != null && service.isLoggedIn) {
|
if (service != null && service.isLoggedIn) {
|
||||||
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
|
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
|
||||||
service.update(track.toDbTrack(), true)
|
service.update(track.toDbTrack(), true)
|
||||||
|
@ -1,33 +1,34 @@
|
|||||||
package eu.kanade.domain.track.service
|
package eu.kanade.domain.track.service
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class TrackPreferences(
|
class TrackPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
|
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
|
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
fun setCredentials(sync: Tracker, username: String, password: String) {
|
||||||
trackUsername(sync).set(username)
|
trackUsername(sync).set(username)
|
||||||
trackPassword(sync).set(password)
|
trackPassword(sync).set(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
|
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
|
||||||
|
|
||||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
|
||||||
|
|
||||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ class UiPreferences(
|
|||||||
|
|
||||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||||
|
|
||||||
|
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
||||||
|
|
||||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||||
|
|
||||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||||
|
@ -7,13 +7,22 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
import eu.kanade.presentation.category.components.CategoryListItem
|
import eu.kanade.presentation.category.components.CategoryListItem
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
@ -27,6 +36,7 @@ import tachiyomi.presentation.core.util.plus
|
|||||||
fun CategoryScreen(
|
fun CategoryScreen(
|
||||||
state: CategoryScreenState.Success,
|
state: CategoryScreenState.Success,
|
||||||
onClickCreate: () -> Unit,
|
onClickCreate: () -> Unit,
|
||||||
|
onClickSortAlphabetically: () -> Unit,
|
||||||
onClickRename: (Category) -> Unit,
|
onClickRename: (Category) -> Unit,
|
||||||
onClickDelete: (Category) -> Unit,
|
onClickDelete: (Category) -> Unit,
|
||||||
onClickMoveUp: (Category) -> Unit,
|
onClickMoveUp: (Category) -> Unit,
|
||||||
@ -36,9 +46,32 @@ fun CategoryScreen(
|
|||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
TopAppBar(
|
||||||
title = stringResource(R.string.action_edit_categories),
|
title = {
|
||||||
navigateUp = navigateUp,
|
Text(
|
||||||
|
text = stringResource(R.string.action_edit_categories),
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigateUp) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
listOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_sort),
|
||||||
|
icon = Icons.Outlined.SortByAlpha,
|
||||||
|
onClick = onClickSortAlphabetically,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -162,7 +162,7 @@ fun CategoryDeleteDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete()
|
onDelete()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -180,6 +180,35 @@ fun CategoryDeleteDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategorySortAlphabeticallyDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSort: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onSort()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_sort_category))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.sort_category_confirmation))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeCategoryDialog(
|
fun ChangeCategoryDialog(
|
||||||
initialSelection: List<CheckboxState<Category>>,
|
initialSelection: List<CheckboxState<Category>>,
|
||||||
@ -217,7 +246,7 @@ fun ChangeCategoryDialog(
|
|||||||
tachiyomi.presentation.core.components.material.TextButton(onClick = {
|
tachiyomi.presentation.core.components.material.TextButton(onClick = {
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
onEditCategories()
|
onEditCategories()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_edit))
|
Text(text = stringResource(R.string.action_edit))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
@ -13,13 +13,18 @@ import java.util.Date
|
|||||||
fun RelativeDateHeader(
|
fun RelativeDateHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
date: Date,
|
date: Date,
|
||||||
|
relativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = remember {
|
text = remember {
|
||||||
date.toRelativeString(context, dateFormat)
|
date.toRelativeString(
|
||||||
|
context,
|
||||||
|
relativeTime,
|
||||||
|
dateFormat,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -98,7 +97,8 @@ private fun HistoryScreenContent(
|
|||||||
onClickDelete: (HistoryWithRelations) -> Unit,
|
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
preferences: UiPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
val relativeTime = remember { preferences.relativeTime().get() }
|
||||||
|
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@ -118,6 +118,7 @@ private fun HistoryScreenContent(
|
|||||||
RelativeDateHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
|
relativeTime = relativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ fun HistoryDeleteDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete(removeEverything)
|
onDelete(removeEverything)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_remove))
|
Text(text = stringResource(R.string.action_remove))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -90,7 +90,7 @@ fun HistoryDeleteAllDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete()
|
onDelete()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -108,13 +108,13 @@ private fun ColumnScope.FilterPage(
|
|||||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||||
)
|
)
|
||||||
|
|
||||||
val trackServices = remember { screenModel.trackServices }
|
val trackers = remember { screenModel.trackers }
|
||||||
when (trackServices.size) {
|
when (trackers.size) {
|
||||||
0 -> {
|
0 -> {
|
||||||
// No trackers
|
// No trackers
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
val service = trackServices[0]
|
val service = trackers[0]
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(R.string.action_filter_tracked),
|
label = stringResource(R.string.action_filter_tracked),
|
||||||
@ -124,7 +124,7 @@ private fun ColumnScope.FilterPage(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
HeadingItem(R.string.action_filter_tracked)
|
HeadingItem(R.string.action_filter_tracked)
|
||||||
trackServices.map { service ->
|
trackers.map { service ->
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = service.name,
|
label = service.name,
|
||||||
|
@ -85,6 +85,7 @@ fun MangaScreen(
|
|||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -140,6 +141,7 @@ fun MangaScreen(
|
|||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = fetchInterval,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
@ -176,6 +178,7 @@ fun MangaScreen(
|
|||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
@ -215,6 +218,7 @@ fun MangaScreen(
|
|||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -264,8 +268,14 @@ private fun MangaScreenSmallImpl(
|
|||||||
|
|
||||||
val chapters = remember(state) { state.processedChapters }
|
val chapters = remember(state) { state.processedChapters }
|
||||||
|
|
||||||
|
val isAnySelected by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
chapters.fastAny { it.selected }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val internalOnBackPressed = {
|
val internalOnBackPressed = {
|
||||||
if (chapters.fastAny { it.selected }) {
|
if (isAnySelected) {
|
||||||
onAllChapterSelected(false)
|
onAllChapterSelected(false)
|
||||||
} else {
|
} else {
|
||||||
onBackClicked()
|
onBackClicked()
|
||||||
@ -275,19 +285,22 @@ private fun MangaScreenSmallImpl(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val firstVisibleItemIndex by remember {
|
val selectedChapterCount: Int = remember(chapters) {
|
||||||
derivedStateOf { chapterListState.firstVisibleItemIndex }
|
chapters.count { it.selected }
|
||||||
}
|
}
|
||||||
val firstVisibleItemScrollOffset by remember {
|
val isFirstItemVisible by remember {
|
||||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
|
derivedStateOf { chapterListState.firstVisibleItemIndex == 0 }
|
||||||
|
}
|
||||||
|
val isFirstItemScrolled by remember {
|
||||||
|
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
|
||||||
}
|
}
|
||||||
val animatedTitleAlpha by animateFloatAsState(
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
if (!isFirstItemVisible) 1f else 0f,
|
||||||
label = "titleAlpha",
|
label = "Top Bar Title",
|
||||||
)
|
)
|
||||||
val animatedBgAlpha by animateFloatAsState(
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||||
label = "bgAlpha",
|
label = "Top Bar Background",
|
||||||
)
|
)
|
||||||
MangaToolbar(
|
MangaToolbar(
|
||||||
title = state.manga.title,
|
title = state.manga.title,
|
||||||
@ -301,14 +314,17 @@ private fun MangaScreenSmallImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
actionModeCounter = chapters.count { it.selected },
|
actionModeCounter = selectedChapterCount,
|
||||||
onSelectAll = { onAllChapterSelected(true) },
|
onSelectAll = { onAllChapterSelected(true) },
|
||||||
onInvertSelection = { onInvertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
val selectedChapters = remember(chapters) {
|
||||||
|
chapters.filter { it.selected }
|
||||||
|
}
|
||||||
SharedMangaBottomActionMenu(
|
SharedMangaBottomActionMenu(
|
||||||
selected = chapters.filter { it.selected },
|
selected = selectedChapters,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@ -319,19 +335,20 @@ private fun MangaScreenSmallImpl(
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
val isFABVisible = remember(chapters) {
|
||||||
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
|
visible = isFABVisible,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
val id = if (state.chapters.fastAny { it.chapter.read }) {
|
val isReading = remember(state.chapters) {
|
||||||
R.string.action_resume
|
state.chapters.fastAny { it.chapter.read }
|
||||||
} else {
|
|
||||||
R.string.action_start
|
|
||||||
}
|
}
|
||||||
Text(text = stringResource(id))
|
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
|
||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
onClick = onContinueReading,
|
onClick = onContinueReading,
|
||||||
@ -345,7 +362,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshingData,
|
refreshing = state.isRefreshingData,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = chapters.fastAll { !it.selected },
|
enabled = !isAnySelected,
|
||||||
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
|
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
@ -417,10 +434,13 @@ private fun MangaScreenSmallImpl(
|
|||||||
key = MangaScreenItem.CHAPTER_HEADER,
|
key = MangaScreenItem.CHAPTER_HEADER,
|
||||||
contentType = MangaScreenItem.CHAPTER_HEADER,
|
contentType = MangaScreenItem.CHAPTER_HEADER,
|
||||||
) {
|
) {
|
||||||
|
val missingChapterCount = remember(chapters) {
|
||||||
|
chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
|
||||||
|
}
|
||||||
ChapterHeader(
|
ChapterHeader(
|
||||||
enabled = chapters.fastAll { !it.selected },
|
enabled = !isAnySelected,
|
||||||
chapterCount = chapters.size,
|
chapterCount = chapters.size,
|
||||||
missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(),
|
missingChapterCount = missingChapterCount,
|
||||||
onClick = onFilterClicked,
|
onClick = onFilterClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -428,6 +448,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
@ -446,6 +467,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -496,12 +518,18 @@ fun MangaScreenLargeImpl(
|
|||||||
|
|
||||||
val chapters = remember(state) { state.processedChapters }
|
val chapters = remember(state) { state.processedChapters }
|
||||||
|
|
||||||
|
val isAnySelected by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
chapters.fastAny { it.selected }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
var topBarHeight by remember { mutableIntStateOf(0) }
|
var topBarHeight by remember { mutableIntStateOf(0) }
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshingData,
|
refreshing = state.isRefreshingData,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = chapters.fastAll { !it.selected },
|
enabled = !isAnySelected,
|
||||||
indicatorPadding = PaddingValues(
|
indicatorPadding = PaddingValues(
|
||||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||||
top = with(density) { topBarHeight.toDp() },
|
top = with(density) { topBarHeight.toDp() },
|
||||||
@ -511,7 +539,7 @@ fun MangaScreenLargeImpl(
|
|||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
|
|
||||||
val internalOnBackPressed = {
|
val internalOnBackPressed = {
|
||||||
if (chapters.fastAny { it.selected }) {
|
if (isAnySelected) {
|
||||||
onAllChapterSelected(false)
|
onAllChapterSelected(false)
|
||||||
} else {
|
} else {
|
||||||
onBackClicked()
|
onBackClicked()
|
||||||
@ -521,10 +549,13 @@ fun MangaScreenLargeImpl(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
val selectedChapterCount = remember(chapters) {
|
||||||
|
chapters.count { it.selected }
|
||||||
|
}
|
||||||
MangaToolbar(
|
MangaToolbar(
|
||||||
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
|
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
|
||||||
title = state.manga.title,
|
title = state.manga.title,
|
||||||
titleAlphaProvider = { if (chapters.fastAny { it.selected }) 1f else 0f },
|
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
|
||||||
backgroundAlphaProvider = { 1f },
|
backgroundAlphaProvider = { 1f },
|
||||||
hasFilters = state.manga.chaptersFiltered(),
|
hasFilters = state.manga.chaptersFiltered(),
|
||||||
onBackClicked = internalOnBackPressed,
|
onBackClicked = internalOnBackPressed,
|
||||||
@ -534,7 +565,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
actionModeCounter = chapters.count { it.selected },
|
actionModeCounter = selectedChapterCount,
|
||||||
onSelectAll = { onAllChapterSelected(true) },
|
onSelectAll = { onAllChapterSelected(true) },
|
||||||
onInvertSelection = { onInvertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
)
|
)
|
||||||
@ -544,8 +575,11 @@ fun MangaScreenLargeImpl(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.BottomEnd,
|
contentAlignment = Alignment.BottomEnd,
|
||||||
) {
|
) {
|
||||||
|
val selectedChapters = remember(chapters) {
|
||||||
|
chapters.filter { it.selected }
|
||||||
|
}
|
||||||
SharedMangaBottomActionMenu(
|
SharedMangaBottomActionMenu(
|
||||||
selected = chapters.filter { it.selected },
|
selected = selectedChapters,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@ -557,19 +591,20 @@ fun MangaScreenLargeImpl(
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
val isFABVisible = remember(chapters) {
|
||||||
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
|
visible = isFABVisible,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
val id = if (state.chapters.fastAny { it.chapter.read }) {
|
val isReading = remember(state.chapters) {
|
||||||
R.string.action_resume
|
state.chapters.fastAny { it.chapter.read }
|
||||||
} else {
|
|
||||||
R.string.action_start
|
|
||||||
}
|
}
|
||||||
Text(text = stringResource(id))
|
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
|
||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
onClick = onContinueReading,
|
onClick = onContinueReading,
|
||||||
@ -640,10 +675,13 @@ fun MangaScreenLargeImpl(
|
|||||||
key = MangaScreenItem.CHAPTER_HEADER,
|
key = MangaScreenItem.CHAPTER_HEADER,
|
||||||
contentType = MangaScreenItem.CHAPTER_HEADER,
|
contentType = MangaScreenItem.CHAPTER_HEADER,
|
||||||
) {
|
) {
|
||||||
|
val missingChapterCount = remember(chapters) {
|
||||||
|
chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
|
||||||
|
}
|
||||||
ChapterHeader(
|
ChapterHeader(
|
||||||
enabled = chapters.fastAll { !it.selected },
|
enabled = !isAnySelected,
|
||||||
chapterCount = chapters.size,
|
chapterCount = chapters.size,
|
||||||
missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(),
|
missingChapterCount = missingChapterCount,
|
||||||
onClick = onFilterButtonClicked,
|
onClick = onFilterButtonClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -651,6 +689,7 @@ fun MangaScreenLargeImpl(
|
|||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
@ -712,6 +751,7 @@ private fun SharedMangaBottomActionMenu(
|
|||||||
private fun LazyListScope.sharedChapterItems(
|
private fun LazyListScope.sharedChapterItems(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<ChapterItem>,
|
chapters: List<ChapterItem>,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -740,7 +780,11 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
date = chapterItem.chapter.dateUpload
|
date = chapterItem.chapter.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(context, dateFormat)
|
Date(it).toRelativeString(
|
||||||
|
context,
|
||||||
|
dateRelativeTime,
|
||||||
|
dateFormat,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
readProgress = chapterItem.chapter.lastPageRead
|
readProgress = chapterItem.chapter.lastPageRead
|
||||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||||
|
@ -143,7 +143,7 @@ fun MangaBottomActionMenu(
|
|||||||
if (onMarkPreviousAsReadClicked != null) {
|
if (onMarkPreviousAsReadClicked != null) {
|
||||||
Button(
|
Button(
|
||||||
title = stringResource(R.string.action_mark_previous_as_read),
|
title = stringResource(R.string.action_mark_previous_as_read),
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
|
icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
|
||||||
toConfirm = confirm[4],
|
toConfirm = confirm[4],
|
||||||
onLongClick = { onLongClickItem(4) },
|
onLongClick = { onLongClickItem(4) },
|
||||||
onClick = onMarkPreviousAsReadClicked,
|
onClick = onMarkPreviousAsReadClicked,
|
||||||
|
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -67,7 +67,7 @@ fun SetIntervalDialog(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..MAX_FETCH_INTERVAL).map {
|
val items = (0..FetchInterval.MAX_INTERVAL).map {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(R.string.label_default)
|
stringResource(R.string.label_default)
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +91,7 @@ fun SetIntervalDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(selectedInterval)
|
onValueChanged(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -286,7 +286,7 @@ fun ExpandableMangaDescription(
|
|||||||
) {
|
) {
|
||||||
tags.forEach {
|
tags.forEach {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
modifier = DefaultTagChipModifier,
|
||||||
text = it,
|
text = it,
|
||||||
onClick = {
|
onClick = {
|
||||||
tagSelected = it
|
tagSelected = it
|
||||||
@ -302,7 +302,7 @@ fun ExpandableMangaDescription(
|
|||||||
) {
|
) {
|
||||||
items(items = tags) {
|
items(items = tags) {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
modifier = DefaultTagChipModifier,
|
||||||
text = it,
|
text = it,
|
||||||
onClick = {
|
onClick = {
|
||||||
tagSelected = it
|
tagSelected = it
|
||||||
@ -654,6 +654,8 @@ private fun MangaSummary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val DefaultTagChipModifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TagsChip(
|
private fun TagsChip(
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -62,7 +62,7 @@ fun MoreScreen(
|
|||||||
WarningBanner(
|
WarningBanner(
|
||||||
textRes = R.string.fdroid_warning,
|
textRes = R.string.fdroid_warning,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri("https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version")
|
uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.more.settings.Preference.PreferenceItem
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import tachiyomi.core.preference.Preference as PreferenceData
|
import tachiyomi.core.preference.Preference as PreferenceData
|
||||||
|
|
||||||
sealed class Preference {
|
sealed class Preference {
|
||||||
@ -133,10 +132,10 @@ sealed class Preference {
|
|||||||
) : PreferenceItem<String>()
|
) : PreferenceItem<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PreferenceItem] for individual tracking service.
|
* A [PreferenceItem] for individual tracker.
|
||||||
*/
|
*/
|
||||||
data class TrackingPreference(
|
data class TrackerPreference(
|
||||||
val service: TrackService,
|
val tracker: Tracker,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
val login: () -> Unit,
|
val login: () -> Unit,
|
||||||
val logout: () -> Unit,
|
val logout: () -> Unit,
|
@ -156,13 +156,13 @@ internal fun PreferenceItem(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.TrackingPreference -> {
|
is Preference.PreferenceItem.TrackerPreference -> {
|
||||||
val uName by Injekt.get<PreferenceStore>()
|
val uName by Injekt.get<PreferenceStore>()
|
||||||
.getString(TrackPreferences.trackUsername(item.service.id))
|
.getString(TrackPreferences.trackUsername(item.tracker.id))
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
item.service.run {
|
item.tracker.run {
|
||||||
TrackingPreferenceWidget(
|
TrackingPreferenceWidget(
|
||||||
service = this,
|
tracker = this,
|
||||||
checked = uName.isNotEmpty(),
|
checked = uName.isNotEmpty(),
|
||||||
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||||
@ -328,7 +328,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val trackManager = remember { Injekt.get<TrackManager>() }
|
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(R.string.label_library),
|
title = stringResource(R.string.label_library),
|
||||||
@ -340,7 +340,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(R.string.pref_refresh_library_tracking),
|
title = stringResource(R.string.pref_refresh_library_tracking),
|
||||||
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
|
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
|
||||||
enabled = trackManager.hasLoggedServices(),
|
enabled = trackerManager.hasLoggedIn(),
|
||||||
onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) },
|
onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
@ -123,6 +123,11 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
|
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
|
||||||
val now = remember { Date().time }
|
val now = remember { Date().time }
|
||||||
|
|
||||||
|
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||||
|
val formattedNow = remember(dateFormat) {
|
||||||
|
UiPreferences.dateFormat(dateFormat).format(now)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentLanguage) {
|
LaunchedEffect(currentLanguage) {
|
||||||
val locale = if (currentLanguage.isEmpty()) {
|
val locale = if (currentLanguage.isEmpty()) {
|
||||||
LocaleListCompat.getEmptyLocaleList()
|
LocaleListCompat.getEmptyLocaleList()
|
||||||
@ -162,6 +167,15 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
"${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)"
|
"${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)"
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = uiPreferences.relativeTime(),
|
||||||
|
title = stringResource(R.string.pref_relative_format),
|
||||||
|
subtitle = stringResource(
|
||||||
|
R.string.pref_relative_format_summary,
|
||||||
|
stringResource(R.string.relative_time_today),
|
||||||
|
formattedNow,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
|||||||
showCreateDialog = false
|
showCreateDialog = false
|
||||||
flag = it
|
flag = it
|
||||||
try {
|
try {
|
||||||
chooseBackupDir.launch(Backup.getBackupFilename())
|
chooseBackupDir.launch(Backup.getFilename())
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
flag = 0
|
flag = 0
|
||||||
context.toast(R.string.file_picker_error)
|
context.toast(R.string.file_picker_error)
|
||||||
@ -250,6 +250,8 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
|||||||
BackupConst.BACKUP_CHAPTER to R.string.chapters,
|
BackupConst.BACKUP_CHAPTER to R.string.chapters,
|
||||||
BackupConst.BACKUP_TRACK to R.string.track,
|
BackupConst.BACKUP_TRACK to R.string.track,
|
||||||
BackupConst.BACKUP_HISTORY to R.string.history,
|
BackupConst.BACKUP_HISTORY to R.string.history,
|
||||||
|
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
|
||||||
|
BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val flags = remember { choices.keys.toMutableStateList() }
|
val flags = remember { choices.keys.toMutableStateList() }
|
||||||
|
@ -23,7 +23,7 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -199,7 +199,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = libraryPreferences.autoUpdateTrackers(),
|
pref = libraryPreferences.autoUpdateTrackers(),
|
||||||
enabled = Injekt.get<TrackManager>().hasLoggedServices(),
|
enabled = Injekt.get<TrackerManager>().hasLoggedIn(),
|
||||||
title = stringResource(R.string.pref_library_update_refresh_trackers),
|
title = stringResource(R.string.pref_library_update_refresh_trackers),
|
||||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||||
),
|
),
|
||||||
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -305,12 +304,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
subtitle = stringResource(R.string.pref_dual_page_invert_summary),
|
subtitle = stringResource(R.string.pref_dual_page_invert_summary),
|
||||||
enabled = dualPageSplit,
|
enabled = dualPageSplit,
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = readerPreferences.longStripSplitWebtoon(),
|
|
||||||
title = stringResource(R.string.pref_long_strip_split),
|
|
||||||
subtitle = stringResource(R.string.split_tall_images_summary),
|
|
||||||
enabled = !isReleaseBuildType, // TODO: Show in release build when the feature is stable
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||||
title = stringResource(R.string.pref_double_tap_zoom),
|
title = stringResource(R.string.pref_double_tap_zoom),
|
||||||
@ -349,11 +342,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
pref = readerPreferences.readWithLongTap(),
|
pref = readerPreferences.readWithLongTap(),
|
||||||
title = stringResource(R.string.pref_read_with_long_tap),
|
title = stringResource(R.string.pref_read_with_long_tap),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = readerPreferences.folderPerManga(),
|
|
||||||
title = stringResource(R.string.pref_create_folder_per_manga),
|
|
||||||
subtitle = stringResource(R.string.pref_create_folder_per_manga_summary),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -44,9 +44,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
||||||
@ -70,7 +70,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun RowScope.AppBarAction() {
|
override fun RowScope.AppBarAction() {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
|
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.HelpOutline,
|
imageVector = Icons.Outlined.HelpOutline,
|
||||||
contentDescription = stringResource(R.string.tracking_guide),
|
contentDescription = stringResource(R.string.tracking_guide),
|
||||||
@ -82,7 +82,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||||
val trackManager = remember { Injekt.get<TrackManager>() }
|
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||||
|
|
||||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||||
@ -90,24 +90,24 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
when (this) {
|
when (this) {
|
||||||
is LoginDialog -> {
|
is LoginDialog -> {
|
||||||
TrackingLoginDialog(
|
TrackingLoginDialog(
|
||||||
service = service,
|
tracker = tracker,
|
||||||
uNameStringRes = uNameStringRes,
|
uNameStringRes = uNameStringRes,
|
||||||
onDismissRequest = { dialog = null },
|
onDismissRequest = { dialog = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is LogoutDialog -> {
|
is LogoutDialog -> {
|
||||||
TrackingLogoutDialog(
|
TrackingLogoutDialog(
|
||||||
service = service,
|
tracker = tracker,
|
||||||
onDismissRequest = { dialog = null },
|
onDismissRequest = { dialog = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val enhancedTrackers = trackManager.services
|
val enhancedTrackers = trackerManager.trackers
|
||||||
.filter { it is EnhancedTrackService }
|
.filter { it is EnhancedTracker }
|
||||||
.partition { service ->
|
.partition { service ->
|
||||||
val acceptedSources = (service as EnhancedTrackService).getAcceptedSources()
|
val acceptedSources = (service as EnhancedTracker).getAcceptedSources()
|
||||||
sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources }
|
sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources }
|
||||||
}
|
}
|
||||||
var enhancedTrackerInfo = stringResource(R.string.enhanced_tracking_info)
|
var enhancedTrackerInfo = stringResource(R.string.enhanced_tracking_info)
|
||||||
@ -127,41 +127,41 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(R.string.services),
|
title = stringResource(R.string.services),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.myAnimeList.name,
|
title = trackerManager.myAnimeList.name,
|
||||||
service = trackManager.myAnimeList,
|
tracker = trackerManager.myAnimeList,
|
||||||
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.myAnimeList) },
|
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.aniList.name,
|
title = trackerManager.aniList.name,
|
||||||
service = trackManager.aniList,
|
tracker = trackerManager.aniList,
|
||||||
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
|
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.aniList) },
|
logout = { dialog = LogoutDialog(trackerManager.aniList) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.kitsu.name,
|
title = trackerManager.kitsu.name,
|
||||||
service = trackManager.kitsu,
|
tracker = trackerManager.kitsu,
|
||||||
login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) },
|
login = { dialog = LoginDialog(trackerManager.kitsu, R.string.email) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.kitsu) },
|
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.mangaUpdates.name,
|
title = trackerManager.mangaUpdates.name,
|
||||||
service = trackManager.mangaUpdates,
|
tracker = trackerManager.mangaUpdates,
|
||||||
login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) },
|
login = { dialog = LoginDialog(trackerManager.mangaUpdates, R.string.username) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.mangaUpdates) },
|
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.shikimori.name,
|
title = trackerManager.shikimori.name,
|
||||||
service = trackManager.shikimori,
|
tracker = trackerManager.shikimori,
|
||||||
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
|
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.shikimori) },
|
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackManager.bangumi.name,
|
title = trackerManager.bangumi.name,
|
||||||
service = trackManager.bangumi,
|
tracker = trackerManager.bangumi,
|
||||||
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
||||||
logout = { dialog = LogoutDialog(trackManager.bangumi) },
|
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(R.string.tracking_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(R.string.tracking_info)),
|
||||||
),
|
),
|
||||||
@ -170,10 +170,10 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
title = stringResource(R.string.enhanced_services),
|
title = stringResource(R.string.enhanced_services),
|
||||||
preferenceItems = enhancedTrackers.first
|
preferenceItems = enhancedTrackers.first
|
||||||
.map { service ->
|
.map { service ->
|
||||||
Preference.PreferenceItem.TrackingPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = service.name,
|
title = service.name,
|
||||||
service = service,
|
tracker = service,
|
||||||
login = { (service as EnhancedTrackService).loginNoop() },
|
login = { (service as EnhancedTracker).loginNoop() },
|
||||||
logout = service::logout,
|
logout = service::logout,
|
||||||
)
|
)
|
||||||
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
|
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
|
||||||
@ -183,15 +183,15 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackingLoginDialog(
|
private fun TrackingLoginDialog(
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
@StringRes uNameStringRes: Int,
|
@StringRes uNameStringRes: Int,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var username by remember { mutableStateOf(TextFieldValue(service.getUsername())) }
|
var username by remember { mutableStateOf(TextFieldValue(tracker.getUsername())) }
|
||||||
var password by remember { mutableStateOf(TextFieldValue(service.getPassword())) }
|
var password by remember { mutableStateOf(TextFieldValue(tracker.getPassword())) }
|
||||||
var processing by remember { mutableStateOf(false) }
|
var processing by remember { mutableStateOf(false) }
|
||||||
var inputError by remember { mutableStateOf(false) }
|
var inputError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
title = {
|
title = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.login_title, service.name),
|
text = stringResource(R.string.login_title, tracker.name),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
IconButton(onClick = onDismissRequest) {
|
IconButton(onClick = onDismissRequest) {
|
||||||
@ -264,7 +264,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
processing = true
|
processing = true
|
||||||
val result = checkLogin(
|
val result = checkLogin(
|
||||||
context = context,
|
context = context,
|
||||||
service = service,
|
tracker = tracker,
|
||||||
username = username.text,
|
username = username.text,
|
||||||
password = password.text,
|
password = password.text,
|
||||||
)
|
)
|
||||||
@ -283,16 +283,16 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
|
|
||||||
private suspend fun checkLogin(
|
private suspend fun checkLogin(
|
||||||
context: Context,
|
context: Context,
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return try {
|
return try {
|
||||||
service.login(username, password)
|
tracker.login(username, password)
|
||||||
withUIContext { context.toast(R.string.login_success) }
|
withUIContext { context.toast(R.string.login_success) }
|
||||||
true
|
true
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
service.logout()
|
tracker.logout()
|
||||||
withUIContext { context.toast(e.message.toString()) }
|
withUIContext { context.toast(e.message.toString()) }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -300,7 +300,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackingLogoutDialog(
|
private fun TrackingLogoutDialog(
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -308,7 +308,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.logout_title, service.name),
|
text = stringResource(R.string.logout_title, tracker.name),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
@ -324,7 +324,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
Button(
|
Button(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onClick = {
|
onClick = {
|
||||||
service.logout()
|
tracker.logout()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
context.toast(R.string.logout_success)
|
context.toast(R.string.logout_success)
|
||||||
},
|
},
|
||||||
@ -342,10 +342,10 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private data class LoginDialog(
|
private data class LoginDialog(
|
||||||
val service: TrackService,
|
val tracker: Tracker,
|
||||||
@StringRes val uNameStringRes: Int,
|
@StringRes val uNameStringRes: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class LogoutDialog(
|
private data class LogoutDialog(
|
||||||
val service: TrackService,
|
val tracker: Tracker,
|
||||||
)
|
)
|
||||||
|
@ -23,12 +23,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import compose.icons.SimpleIcons
|
|
||||||
import compose.icons.simpleicons.Discord
|
|
||||||
import compose.icons.simpleicons.Facebook
|
|
||||||
import compose.icons.simpleicons.Github
|
|
||||||
import compose.icons.simpleicons.Reddit
|
|
||||||
import compose.icons.simpleicons.Twitter
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.more.LogoHeader
|
import eu.kanade.presentation.more.LogoHeader
|
||||||
@ -53,6 +47,12 @@ import tachiyomi.domain.release.interactor.GetApplicationRelease
|
|||||||
import tachiyomi.presentation.core.components.LinkIcon
|
import tachiyomi.presentation.core.components.LinkIcon
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.icons.CustomIcons
|
||||||
|
import tachiyomi.presentation.core.icons.Discord
|
||||||
|
import tachiyomi.presentation.core.icons.Facebook
|
||||||
|
import tachiyomi.presentation.core.icons.Github
|
||||||
|
import tachiyomi.presentation.core.icons.Reddit
|
||||||
|
import tachiyomi.presentation.core.icons.X
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -149,7 +149,7 @@ object AboutScreen : Screen() {
|
|||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.help_translate),
|
title = stringResource(R.string.help_translate),
|
||||||
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") },
|
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/docs/contribute#translation") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ object AboutScreen : Screen() {
|
|||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy") },
|
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy/") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,27 +181,27 @@ object AboutScreen : Screen() {
|
|||||||
)
|
)
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "Discord",
|
label = "Discord",
|
||||||
icon = SimpleIcons.Discord,
|
icon = CustomIcons.Discord,
|
||||||
url = "https://discord.gg/tachiyomi",
|
url = "https://discord.gg/tachiyomi",
|
||||||
)
|
)
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "Twitter",
|
label = "X",
|
||||||
icon = SimpleIcons.Twitter,
|
icon = CustomIcons.X,
|
||||||
url = "https://twitter.com/tachiyomiorg",
|
url = "https://x.com/tachiyomiorg",
|
||||||
)
|
)
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "Facebook",
|
label = "Facebook",
|
||||||
icon = SimpleIcons.Facebook,
|
icon = CustomIcons.Facebook,
|
||||||
url = "https://facebook.com/tachiyomiorg",
|
url = "https://facebook.com/tachiyomiorg",
|
||||||
)
|
)
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "Reddit",
|
label = "Reddit",
|
||||||
icon = SimpleIcons.Reddit,
|
icon = CustomIcons.Reddit,
|
||||||
url = "https://www.reddit.com/r/Tachiyomi",
|
url = "https://www.reddit.com/r/Tachiyomi",
|
||||||
)
|
)
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "GitHub",
|
label = "GitHub",
|
||||||
icon = SimpleIcons.Github,
|
icon = CustomIcons.Github,
|
||||||
url = "https://github.com/tachiyomiorg",
|
url = "https://github.com/tachiyomiorg",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,9 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
),
|
),
|
||||||
onLibraryClick = {
|
onLibraryClick = {
|
||||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||||
name = it.name,
|
name = it.library.name,
|
||||||
website = it.website,
|
website = it.library.website,
|
||||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||||
)
|
)
|
||||||
navigator.push(libraryLicenseScreen)
|
navigator.push(libraryLicenseScreen)
|
||||||
},
|
},
|
||||||
|
@ -20,12 +20,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackingPreferenceWidget(
|
fun TrackingPreferenceWidget(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
@ -38,9 +38,9 @@ fun TrackingPreferenceWidget(
|
|||||||
.padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp),
|
.padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
TrackLogoIcon(service)
|
TrackLogoIcon(tracker)
|
||||||
Text(
|
Text(
|
||||||
text = service.name,
|
text = tracker.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
package eu.kanade.presentation.reader
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.manga.model.orientationType
|
import eu.kanade.domain.manga.model.orientationType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||||
|
|
||||||
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
||||||
|
|
||||||
@ -32,22 +32,20 @@ fun OrientationModeSelectDialog(
|
|||||||
val manga by screenModel.mangaFlow.collectAsState()
|
val manga by screenModel.mangaFlow.collectAsState()
|
||||||
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
|
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||||
) {
|
SettingsIconGrid(R.string.rotation_type) {
|
||||||
Row(
|
items(orientationTypeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == orientationType,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.rotation_type) {
|
screenModel.onChangeOrientation(mode)
|
||||||
orientationTypeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == orientationType,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeOrientation(it)
|
|
||||||
onChange(stringRes)
|
onChange(stringRes)
|
||||||
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(stringRes)) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||||
|
title = stringResource(stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.text.ExperimentalTextApi
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@OptIn(ExperimentalTextApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PageIndicatorText(
|
fun PageIndicatorText(
|
||||||
currentPage: Int,
|
currentPage: Int,
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
package eu.kanade.presentation.reader
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.res.vectorResource
|
||||||
import eu.kanade.domain.manga.model.readingModeType
|
import eu.kanade.domain.manga.model.readingModeType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||||
|
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
||||||
@ -32,22 +33,20 @@ fun ReadingModeSelectDialog(
|
|||||||
val manga by screenModel.mangaFlow.collectAsState()
|
val manga by screenModel.mangaFlow.collectAsState()
|
||||||
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
|
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
|
||||||
) {
|
SettingsIconGrid(R.string.pref_category_reading_mode) {
|
||||||
Row(
|
items(readingModeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == readingMode,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.pref_category_reading_mode) {
|
screenModel.onChangeReadingMode(mode)
|
||||||
readingModeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == readingMode,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeReadingMode(it)
|
|
||||||
onChange(stringRes)
|
onChange(stringRes)
|
||||||
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(stringRes)) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||||
|
title = stringResource(stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ 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 eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
import tachiyomi.presentation.core.components.HeadingItem
|
import tachiyomi.presentation.core.components.HeadingItem
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||||
@ -185,13 +184,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isReleaseBuildType) {
|
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(R.string.pref_long_strip_split),
|
|
||||||
pref = screenModel.preferences.longStripSplitWebtoon(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(R.string.pref_double_tap_zoom),
|
label = stringResource(R.string.pref_double_tap_zoom),
|
||||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||||
|
@ -49,7 +49,7 @@ import eu.kanade.domain.track.model.toDbTrack
|
|||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -80,12 +80,12 @@ fun TrackInfoDialogHome(
|
|||||||
) {
|
) {
|
||||||
trackItems.forEach { item ->
|
trackItems.forEach { item ->
|
||||||
if (item.track != null) {
|
if (item.track != null) {
|
||||||
val supportsScoring = item.service.getScoreList().isNotEmpty()
|
val supportsScoring = item.tracker.getScoreList().isNotEmpty()
|
||||||
val supportsReadingDates = item.service.supportsReadingDates
|
val supportsReadingDates = item.tracker.supportsReadingDates
|
||||||
TrackInfoItem(
|
TrackInfoItem(
|
||||||
title = item.track.title,
|
title = item.track.title,
|
||||||
service = item.service,
|
tracker = item.tracker,
|
||||||
status = item.service.getStatus(item.track.status.toInt()),
|
status = item.tracker.getStatus(item.track.status.toInt()),
|
||||||
onStatusClick = { onStatusClick(item) },
|
onStatusClick = { onStatusClick(item) },
|
||||||
chapters = "${item.track.lastChapterRead.toInt()}".let {
|
chapters = "${item.track.lastChapterRead.toInt()}".let {
|
||||||
val totalChapters = item.track.totalChapters
|
val totalChapters = item.track.totalChapters
|
||||||
@ -97,7 +97,7 @@ fun TrackInfoDialogHome(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChaptersClick = { onChapterClick(item) },
|
onChaptersClick = { onChapterClick(item) },
|
||||||
score = item.service.displayScore(item.track.toDbTrack())
|
score = item.tracker.displayScore(item.track.toDbTrack())
|
||||||
.takeIf { supportsScoring && item.track.score != 0.0 },
|
.takeIf { supportsScoring && item.track.score != 0.0 },
|
||||||
onScoreClick = { onScoreClick(item) }
|
onScoreClick = { onScoreClick(item) }
|
||||||
.takeIf { supportsScoring },
|
.takeIf { supportsScoring },
|
||||||
@ -115,7 +115,7 @@ fun TrackInfoDialogHome(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TrackInfoItemEmpty(
|
TrackInfoItemEmpty(
|
||||||
service = item.service,
|
tracker = item.tracker,
|
||||||
onNewSearch = { onNewSearch(item) },
|
onNewSearch = { onNewSearch(item) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ fun TrackInfoDialogHome(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TrackInfoItem(
|
private fun TrackInfoItem(
|
||||||
title: String,
|
title: String,
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
@StringRes status: Int?,
|
@StringRes status: Int?,
|
||||||
onStatusClick: () -> Unit,
|
onStatusClick: () -> Unit,
|
||||||
chapters: String,
|
chapters: String,
|
||||||
@ -147,7 +147,7 @@ private fun TrackInfoItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
TrackLogoIcon(
|
TrackLogoIcon(
|
||||||
service = service,
|
tracker = tracker,
|
||||||
onClick = onOpenInBrowser,
|
onClick = onOpenInBrowser,
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
@ -260,13 +260,13 @@ private fun TrackDetailsItem(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackInfoItemEmpty(
|
private fun TrackInfoItemEmpty(
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
onNewSearch: () -> Unit,
|
onNewSearch: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
TrackLogoIcon(service)
|
TrackLogoIcon(tracker)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onNewSearch,
|
onClick = onNewSearch,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -70,7 +70,7 @@ import tachiyomi.presentation.core.util.runOnEnterKeyPressed
|
|||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackServiceSearch(
|
fun TrackerSearch(
|
||||||
query: TextFieldValue,
|
query: TextFieldValue,
|
||||||
onQueryChange: (TextFieldValue) -> Unit,
|
onQueryChange: (TextFieldValue) -> Unit,
|
||||||
onDispatchQuery: () -> Unit,
|
onDispatchQuery: () -> Unit,
|
||||||
@ -223,6 +223,7 @@ private fun SearchResultItem(
|
|||||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackLogoIcon(
|
fun TrackLogoIcon(
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val modifier = if (onClick != null) {
|
val modifier = if (onClick != null) {
|
||||||
@ -29,13 +29,13 @@ fun TrackLogoIcon(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.background(color = Color(service.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(service.getLogo()),
|
painter = painterResource(tracker.getLogo()),
|
||||||
contentDescription = service.name,
|
contentDescription = tracker.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ fun UpdatesDeleteConfirmationDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onConfirm()
|
onConfirm()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -43,6 +43,7 @@ fun UpdateScreen(
|
|||||||
state: UpdatesScreenModel.State,
|
state: UpdatesScreenModel.State,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
|
relativeTime: Boolean,
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
@ -113,7 +114,7 @@ fun UpdateScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatesUiItems(
|
updatesUiItems(
|
||||||
uiModels = state.getUiModel(context),
|
uiModels = state.getUiModel(context, relativeTime),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
|
@ -175,7 +175,7 @@ fun WebViewScreenContent(
|
|||||||
WarningBanner(
|
WarningBanner(
|
||||||
textRes = R.string.information_cloudflare_help,
|
textRes = R.string.information_cloudflare_help,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues")
|
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.JavaScriptEngine
|
import eu.kanade.tachiyomi.network.JavaScriptEngine
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
@ -134,7 +134,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { DownloadManager(app) }
|
||||||
addSingletonFactory { DownloadCache(app) }
|
addSingletonFactory { DownloadCache(app) }
|
||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackerManager() }
|
||||||
addSingletonFactory { DelayedTrackingStore(app) }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
addSingletonFactory { ImageSaver(app) }
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
@ -9,15 +9,15 @@ import eu.kanade.domain.ui.UiPreferences
|
|||||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import tachiyomi.core.preference.getAndSet
|
import tachiyomi.core.preference.getAndSet
|
||||||
@ -47,7 +47,7 @@ object Migrations {
|
|||||||
libraryPreferences: LibraryPreferences,
|
libraryPreferences: LibraryPreferences,
|
||||||
readerPreferences: ReaderPreferences,
|
readerPreferences: ReaderPreferences,
|
||||||
backupPreferences: BackupPreferences,
|
backupPreferences: BackupPreferences,
|
||||||
trackManager: TrackManager,
|
trackerManager: TrackerManager,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val lastVersionCode = preferenceStore.getInt("last_version_code", 0)
|
val lastVersionCode = preferenceStore.getInt("last_version_code", 0)
|
||||||
val oldVersion = lastVersionCode.get()
|
val oldVersion = lastVersionCode.get()
|
||||||
@ -135,8 +135,8 @@ object Migrations {
|
|||||||
// Force MAL log out due to login flow change
|
// Force MAL log out due to login flow change
|
||||||
// v52: switched from scraping to WebView
|
// v52: switched from scraping to WebView
|
||||||
// v53: switched from WebView to OAuth
|
// v53: switched from WebView to OAuth
|
||||||
if (trackManager.myAnimeList.isLoggedIn) {
|
if (trackerManager.myAnimeList.isLoggedIn) {
|
||||||
trackManager.myAnimeList.logout()
|
trackerManager.myAnimeList.logout()
|
||||||
context.toast(R.string.myanimelist_relogin)
|
context.toast(R.string.myanimelist_relogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,7 +342,7 @@ object Migrations {
|
|||||||
"pref_filter_library_started",
|
"pref_filter_library_started",
|
||||||
"pref_filter_library_bookmarked",
|
"pref_filter_library_bookmarked",
|
||||||
"pref_filter_library_completed",
|
"pref_filter_library_completed",
|
||||||
) + trackManager.services.map { "pref_filter_library_tracked_${it.id}" }
|
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
|
||||||
|
|
||||||
prefKeys.forEach { key ->
|
prefKeys.forEach { key ->
|
||||||
val pref = preferenceStore.getInt(key, 0)
|
val pref = preferenceStore.getInt(key, 0)
|
||||||
@ -362,19 +362,31 @@ object Migrations {
|
|||||||
if (oldVersion < 100) {
|
if (oldVersion < 100) {
|
||||||
BackupCreateJob.setupTask(context)
|
BackupCreateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
if (oldVersion < 102) {
|
|
||||||
// This was accidentally visible from the reader settings sheet, but should always
|
|
||||||
// be disabled in release builds.
|
|
||||||
if (isReleaseBuildType) {
|
|
||||||
readerPreferences.longStripSplitWebtoon().set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 105) {
|
if (oldVersion < 105) {
|
||||||
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
||||||
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
||||||
pref.getAndSet { it - "battery_not_low" }
|
pref.getAndSet { it - "battery_not_low" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 106) {
|
||||||
|
val pref = preferenceStore.getInt("relative_time", 7)
|
||||||
|
if (pref.get() == 0) {
|
||||||
|
uiPreferences.relativeTime().set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 107) {
|
||||||
|
preferenceStore.getAll()
|
||||||
|
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
|
||||||
|
.forEach { (key, value) ->
|
||||||
|
if (value is String) {
|
||||||
|
preferenceStore
|
||||||
|
.getString(Preference.privateKey(key))
|
||||||
|
.set(value)
|
||||||
|
|
||||||
|
preferenceStore.getString(key).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,21 @@ package eu.kanade.tachiyomi.data.backup
|
|||||||
internal object BackupConst {
|
internal object BackupConst {
|
||||||
const val BACKUP_CATEGORY = 0x1
|
const val BACKUP_CATEGORY = 0x1
|
||||||
const val BACKUP_CATEGORY_MASK = 0x1
|
const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
|
||||||
const val BACKUP_CHAPTER = 0x2
|
const val BACKUP_CHAPTER = 0x2
|
||||||
const val BACKUP_CHAPTER_MASK = 0x2
|
const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
|
||||||
const val BACKUP_HISTORY = 0x4
|
const val BACKUP_HISTORY = 0x4
|
||||||
const val BACKUP_HISTORY_MASK = 0x4
|
const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
|
||||||
const val BACKUP_TRACK = 0x8
|
const val BACKUP_TRACK = 0x8
|
||||||
const val BACKUP_TRACK_MASK = 0x8
|
const val BACKUP_TRACK_MASK = 0x8
|
||||||
const val BACKUP_ALL = 0xF
|
|
||||||
|
const val BACKUP_APP_PREFS = 0x10
|
||||||
|
const val BACKUP_APP_PREFS_MASK = 0x10
|
||||||
|
|
||||||
|
const val BACKUP_SOURCE_PREFS = 0x20
|
||||||
|
const val BACKUP_SOURCE_PREFS_MASK = 0x20
|
||||||
|
|
||||||
|
const val BACKUP_ALL = 0x3F
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
|
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
|
||||||
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -0,0 +1,268 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.preferenceKey
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasPermission
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class BackupCreator(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val handler: DatabaseHandler = Injekt.get()
|
||||||
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
|
private val backupPreferences: BackupPreferences = Injekt.get()
|
||||||
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
|
private val getFavorites: GetFavorites = Injekt.get()
|
||||||
|
private val getHistory: GetHistory = Injekt.get()
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
|
||||||
|
internal val parser = ProtoBuf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup file.
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
|
*/
|
||||||
|
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
|
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseManga = getFavorites.await()
|
||||||
|
val backup = Backup(
|
||||||
|
backupMangas(databaseManga, flags),
|
||||||
|
backupCategories(flags),
|
||||||
|
emptyList(),
|
||||||
|
prepExtensionInfoForSync(databaseManga),
|
||||||
|
backupAppPreferences(flags),
|
||||||
|
backupSourcePreferences(flags),
|
||||||
|
)
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
|
try {
|
||||||
|
file = (
|
||||||
|
if (isAutoBackup) {
|
||||||
|
// Get dir of file and create
|
||||||
|
var dir = UniFile.fromUri(context, uri)
|
||||||
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
||||||
|
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
dir.createFile(Backup.getFilename())
|
||||||
|
} else {
|
||||||
|
UniFile.fromUri(context, uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
||||||
|
|
||||||
|
if (!file.isFile) {
|
||||||
|
throw IllegalStateException("Failed to get handle on a backup file")
|
||||||
|
}
|
||||||
|
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.openOutputStream().also {
|
||||||
|
// Force overwrite old file
|
||||||
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
|
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
BackupFileValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
file?.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map(Manga::source)
|
||||||
|
.distinct()
|
||||||
|
.map(sourceManager::getOrStub)
|
||||||
|
.map(BackupSource::copyFrom)
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @return list of [BackupCategory] to be backed up
|
||||||
|
*/
|
||||||
|
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
getCategories.await()
|
||||||
|
.filterNot(Category::isSystemCategory)
|
||||||
|
.map(backupCategoryMapper)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
|
return mangas.map {
|
||||||
|
backupManga(it, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @param options options for the backup
|
||||||
|
* @return [BackupManga] containing manga in a serializable form
|
||||||
|
*/
|
||||||
|
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
||||||
|
// Entry for this manga
|
||||||
|
val mangaObject = BackupManga.copyFrom(manga)
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
mangaObject.chapters = chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = getCategories.await(manga.id)
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
mangaObject.categories = categoriesForManga.map { it.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
mangaObject.tracking = tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
|
val history = historyByMangaId.map { history ->
|
||||||
|
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||||
|
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||||
|
}
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
mangaObject.history = history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaObject
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
||||||
|
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
|
||||||
|
|
||||||
|
return preferenceStore.getAll().toBackupPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
||||||
|
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
|
||||||
|
|
||||||
|
return sourceManager.getOnlineSources()
|
||||||
|
.filterIsInstance<ConfigurableSource>()
|
||||||
|
.map {
|
||||||
|
BackupSourcePreferences(
|
||||||
|
it.preferenceKey(),
|
||||||
|
it.sourcePreferences().all.toBackupPreferences(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||||
|
return this.filterKeys { !Preference.isPrivate(it) }
|
||||||
|
.mapNotNull { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
||||||
|
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
||||||
|
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
||||||
|
is String -> BackupPreference(key, StringPreferenceValue(value))
|
||||||
|
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
||||||
|
is Set<*> -> (value as? Set<String>)?.let {
|
||||||
|
BackupPreference(key, StringSetPreferenceValue(it))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ 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.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
import eu.kanade.tachiyomi.util.BackupUtil
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -11,7 +11,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
class BackupFileValidator(
|
class BackupFileValidator(
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackerManager: TrackerManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +50,7 @@ class BackupFileValidator(
|
|||||||
.map { it.syncId }
|
.map { it.syncId }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it.toLong()) }
|
.mapNotNull { trackerManager.get(it.toLong()) }
|
||||||
.filter { !it.isLoggedIn }
|
.filter { !it.isLoggedIn }
|
||||||
.map { it.name }
|
.map { it.name }
|
||||||
.sorted()
|
.sorted()
|
||||||
@ -58,5 +58,8 @@ class BackupFileValidator(
|
|||||||
return Results(missingSources, missingTrackers)
|
return Results(missingSources, missingTrackers)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class Results(
|
||||||
|
val missingSources: List<String>,
|
||||||
|
val missingTrackers: List<String>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,590 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.domain.chapter.model.copyFrom
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
|
||||||
import eu.kanade.tachiyomi.source.model.copyFrom
|
|
||||||
import eu.kanade.tachiyomi.util.system.hasPermission
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
|
||||||
import logcat.LogPriority
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import okio.sink
|
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
import tachiyomi.data.Manga_sync
|
|
||||||
import tachiyomi.data.Mangas
|
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
|
||||||
import tachiyomi.domain.category.model.Category
|
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
|
||||||
import tachiyomi.domain.history.model.HistoryUpdate
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Date
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class BackupManager(
|
|
||||||
private val context: Context,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val handler: DatabaseHandler = Injekt.get()
|
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
|
||||||
private val backupPreferences: BackupPreferences = Injekt.get()
|
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
|
||||||
private val getCategories: GetCategories = Injekt.get()
|
|
||||||
private val getFavorites: GetFavorites = Injekt.get()
|
|
||||||
private val getHistory: GetHistory = Injekt.get()
|
|
||||||
|
|
||||||
internal val parser = ProtoBuf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create backup file from database
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isAutoBackup backup called from scheduled backup job
|
|
||||||
*/
|
|
||||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
|
||||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
|
||||||
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
|
||||||
}
|
|
||||||
|
|
||||||
val databaseManga = getFavorites.await()
|
|
||||||
val backup = Backup(
|
|
||||||
backupMangas(databaseManga, flags),
|
|
||||||
backupCategories(flags),
|
|
||||||
emptyList(),
|
|
||||||
prepExtensionInfoForSync(databaseManga),
|
|
||||||
)
|
|
||||||
|
|
||||||
var file: UniFile? = null
|
|
||||||
try {
|
|
||||||
file = (
|
|
||||||
if (isAutoBackup) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
|
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
dir.createFile(Backup.getBackupFilename())
|
|
||||||
} else {
|
|
||||||
UniFile.fromUri(context, uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
|
||||||
|
|
||||||
if (!file.isFile) {
|
|
||||||
throw IllegalStateException("Failed to get handle on a backup file")
|
|
||||||
}
|
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
|
||||||
if (byteArray.isEmpty()) {
|
|
||||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
|
||||||
}
|
|
||||||
|
|
||||||
file.openOutputStream().also {
|
|
||||||
// Force overwrite old file
|
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
|
||||||
}.sink().gzip().buffer().use { it.write(byteArray) }
|
|
||||||
val fileUri = file.uri
|
|
||||||
|
|
||||||
// Make sure it's a valid backup file
|
|
||||||
BackupFileValidator().validate(context, fileUri)
|
|
||||||
|
|
||||||
return fileUri.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
file?.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
|
||||||
return mangas
|
|
||||||
.asSequence()
|
|
||||||
.map(Manga::source)
|
|
||||||
.distinct()
|
|
||||||
.map(sourceManager::getOrStub)
|
|
||||||
.map(BackupSource::copyFrom)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @return list of [BackupCategory] to be backed up
|
|
||||||
*/
|
|
||||||
suspend fun backupCategories(options: Int): List<BackupCategory> {
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
getCategories.await()
|
|
||||||
.filterNot(Category::isSystemCategory)
|
|
||||||
.map(backupCategoryMapper)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
|
||||||
return mangas.map {
|
|
||||||
backupManga(it, flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @param options options for the backup
|
|
||||||
* @return [BackupManga] containing manga in a serializable form
|
|
||||||
*/
|
|
||||||
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
|
||||||
// Entry for this manga
|
|
||||||
val mangaObject = BackupManga.copyFrom(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
mangaObject.chapters = chapters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = getCategories.await(manga.id)
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
mangaObject.categories = categoriesForManga.map { it.order }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
mangaObject.tracking = tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyByMangaId = getHistory.await(manga.id)
|
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
|
||||||
val history = historyByMangaId.map { history ->
|
|
||||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
|
||||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
|
||||||
}
|
|
||||||
if (history.isNotEmpty()) {
|
|
||||||
mangaObject.history = history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaObject
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
|
||||||
var updatedManga = manga.copy(id = dbManga._id)
|
|
||||||
updatedManga = updatedManga.copyFrom(dbManga)
|
|
||||||
updateManga(updatedManga)
|
|
||||||
return updatedManga
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga info.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreNewManga(manga: Manga): Manga {
|
|
||||||
return manga.copy(
|
|
||||||
initialized = manga.description != null,
|
|
||||||
id = insertManga(manga),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the categories from Json
|
|
||||||
*
|
|
||||||
* @param backupCategories list containing categories
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
|
||||||
// Get categories from file and from db
|
|
||||||
val dbCategories = getCategories.await()
|
|
||||||
|
|
||||||
val categories = backupCategories.map {
|
|
||||||
var category = it.getCategory()
|
|
||||||
var found = false
|
|
||||||
for (dbCategory in dbCategories) {
|
|
||||||
// If the category is already in the db, assign the id to the file's category
|
|
||||||
// and do nothing
|
|
||||||
if (category.name == dbCategory.name) {
|
|
||||||
category = category.copy(id = dbCategory.id)
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
// Let the db assign the id
|
|
||||||
val id = handler.awaitOneExecutable {
|
|
||||||
categoriesQueries.insert(category.name, category.order, category.flags)
|
|
||||||
categoriesQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
category = category.copy(id = id)
|
|
||||||
}
|
|
||||||
|
|
||||||
category
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryPreferences.categorizedDisplaySettings().set(
|
|
||||||
(dbCategories + categories)
|
|
||||||
.distinctBy { it.flags }
|
|
||||||
.size > 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories a manga is in.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose categories have to be restored.
|
|
||||||
* @param categories the categories to restore.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
|
||||||
val dbCategories = getCategories.await()
|
|
||||||
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
|
||||||
|
|
||||||
categories.forEach { backupCategoryOrder ->
|
|
||||||
backupCategories.firstOrNull {
|
|
||||||
it.order == backupCategoryOrder.toLong()
|
|
||||||
}?.let { backupCategory ->
|
|
||||||
dbCategories.firstOrNull { dbCategory ->
|
|
||||||
dbCategory.name == backupCategory.name
|
|
||||||
}?.let { dbCategory ->
|
|
||||||
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
|
||||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
|
||||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore history from Json
|
|
||||||
*
|
|
||||||
* @param history list containing history to be restored
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreHistory(history: List<BackupHistory>) {
|
|
||||||
// List containing history to be updated
|
|
||||||
val toUpdate = mutableListOf<HistoryUpdate>()
|
|
||||||
for ((url, lastRead, readDuration) in history) {
|
|
||||||
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
|
||||||
// Check if history already in database and update
|
|
||||||
if (dbHistory != null) {
|
|
||||||
dbHistory = dbHistory.copy(
|
|
||||||
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
|
||||||
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
|
||||||
)
|
|
||||||
toUpdate.add(
|
|
||||||
HistoryUpdate(
|
|
||||||
chapterId = dbHistory.chapter_id,
|
|
||||||
readAt = dbHistory.last_read!!,
|
|
||||||
sessionReadDuration = dbHistory.time_read,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// If not in database create
|
|
||||||
handler
|
|
||||||
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
|
||||||
?.let {
|
|
||||||
toUpdate.add(
|
|
||||||
HistoryUpdate(
|
|
||||||
chapterId = it._id,
|
|
||||||
readAt = Date(lastRead),
|
|
||||||
sessionReadDuration = readDuration,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler.await(true) {
|
|
||||||
toUpdate.forEach { payload ->
|
|
||||||
historyQueries.upsert(
|
|
||||||
payload.chapterId,
|
|
||||||
payload.readAt,
|
|
||||||
payload.sessionReadDuration,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the sync of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose sync have to be restored.
|
|
||||||
* @param tracks the track list to restore.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreTracking(manga: Manga, tracks: List<tachiyomi.domain.track.model.Track>) {
|
|
||||||
// Get tracks from database
|
|
||||||
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
|
||||||
val toUpdate = mutableListOf<Manga_sync>()
|
|
||||||
val toInsert = mutableListOf<tachiyomi.domain.track.model.Track>()
|
|
||||||
|
|
||||||
tracks
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
.map { it.copy(mangaId = manga.id) }
|
|
||||||
.forEach { track ->
|
|
||||||
var isInDatabase = false
|
|
||||||
for (dbTrack in dbTracks) {
|
|
||||||
if (track.syncId == dbTrack.sync_id) {
|
|
||||||
// The sync is already in the db, only update its fields
|
|
||||||
var temp = dbTrack
|
|
||||||
if (track.remoteId != dbTrack.remote_id) {
|
|
||||||
temp = temp.copy(remote_id = track.remoteId)
|
|
||||||
}
|
|
||||||
if (track.libraryId != dbTrack.library_id) {
|
|
||||||
temp = temp.copy(library_id = track.libraryId)
|
|
||||||
}
|
|
||||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
|
||||||
isInDatabase = true
|
|
||||||
toUpdate.add(temp)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
toInsert.add(track.copy(id = 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (toUpdate.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
toUpdate.forEach { track ->
|
|
||||||
manga_syncQueries.update(
|
|
||||||
track.manga_id,
|
|
||||||
track.sync_id,
|
|
||||||
track.remote_id,
|
|
||||||
track.library_id,
|
|
||||||
track.title,
|
|
||||||
track.last_chapter_read,
|
|
||||||
track.total_chapters,
|
|
||||||
track.status,
|
|
||||||
track.score,
|
|
||||||
track.remote_url,
|
|
||||||
track.start_date,
|
|
||||||
track.finish_date,
|
|
||||||
track._id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (toInsert.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
toInsert.forEach { track ->
|
|
||||||
manga_syncQueries.insert(
|
|
||||||
track.mangaId,
|
|
||||||
track.syncId,
|
|
||||||
track.remoteId,
|
|
||||||
track.libraryId,
|
|
||||||
track.title,
|
|
||||||
track.lastChapterRead,
|
|
||||||
track.totalChapters,
|
|
||||||
track.status,
|
|
||||||
track.score,
|
|
||||||
track.remoteUrl,
|
|
||||||
track.startDate,
|
|
||||||
track.finishDate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun restoreChapters(manga: Manga, chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
|
|
||||||
|
|
||||||
val processed = chapters.map { chapter ->
|
|
||||||
var updatedChapter = chapter
|
|
||||||
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
|
||||||
if (dbChapter != null) {
|
|
||||||
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
|
||||||
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
|
||||||
if (dbChapter.read != chapter.read) {
|
|
||||||
updatedChapter = updatedChapter.copy(read = chapter.read, lastPageRead = chapter.lastPageRead)
|
|
||||||
} else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
|
||||||
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
|
||||||
}
|
|
||||||
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
|
||||||
updatedChapter = updatedChapter.copy(bookmark = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedChapter.copy(mangaId = manga.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newChapters = processed.groupBy { it.id > 0 }
|
|
||||||
newChapters[true]?.let { updateKnownChapters(it) }
|
|
||||||
newChapters[false]?.let { insertChapters(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns manga
|
|
||||||
*
|
|
||||||
* @return [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
|
||||||
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
private suspend fun insertManga(manga: Manga): Long {
|
|
||||||
return handler.awaitOneExecutable(true) {
|
|
||||||
mangasQueries.insert(
|
|
||||||
source = manga.source,
|
|
||||||
url = manga.url,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre,
|
|
||||||
title = manga.title,
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
lastUpdate = manga.lastUpdate,
|
|
||||||
nextUpdate = 0L,
|
|
||||||
calculateInterval = 0L,
|
|
||||||
initialized = manga.initialized,
|
|
||||||
viewerFlags = manga.viewerFlags,
|
|
||||||
chapterFlags = manga.chapterFlags,
|
|
||||||
coverLastModified = manga.coverLastModified,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
updateStrategy = manga.updateStrategy,
|
|
||||||
)
|
|
||||||
mangasQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateManga(manga: Manga): Long {
|
|
||||||
handler.await(true) {
|
|
||||||
mangasQueries.update(
|
|
||||||
source = manga.source,
|
|
||||||
url = manga.url,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre?.joinToString(separator = ", "),
|
|
||||||
title = manga.title,
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
lastUpdate = manga.lastUpdate,
|
|
||||||
nextUpdate = null,
|
|
||||||
calculateInterval = null,
|
|
||||||
initialized = manga.initialized,
|
|
||||||
viewer = manga.viewerFlags,
|
|
||||||
chapterFlags = manga.chapterFlags,
|
|
||||||
coverLastModified = manga.coverLastModified,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
mangaId = manga.id,
|
|
||||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
private suspend fun insertChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
handler.await(true) {
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
chaptersQueries.insert(
|
|
||||||
chapter.mangaId,
|
|
||||||
chapter.url,
|
|
||||||
chapter.name,
|
|
||||||
chapter.scanlator,
|
|
||||||
chapter.read,
|
|
||||||
chapter.bookmark,
|
|
||||||
chapter.lastPageRead,
|
|
||||||
chapter.chapterNumber,
|
|
||||||
chapter.sourceOrder,
|
|
||||||
chapter.dateFetch,
|
|
||||||
chapter.dateUpload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a list of chapters with known database ids
|
|
||||||
*/
|
|
||||||
private suspend fun updateKnownChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
handler.await(true) {
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
chaptersQueries.update(
|
|
||||||
mangaId = null,
|
|
||||||
url = null,
|
|
||||||
name = null,
|
|
||||||
scanlator = null,
|
|
||||||
read = chapter.read,
|
|
||||||
bookmark = chapter.bookmark,
|
|
||||||
lastPageRead = chapter.lastPageRead,
|
|
||||||
chapterNumber = null,
|
|
||||||
sourceOrder = null,
|
|
||||||
dateFetch = null,
|
|
||||||
dateUpload = null,
|
|
||||||
chapterId = chapter.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -26,7 +26,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
val sync = inputData.getBoolean(SYNC, false)
|
val sync = inputData.getBoolean(SYNC_KEY, false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setForeground(getForegroundInfo())
|
setForeground(getForegroundInfo())
|
||||||
@ -67,7 +67,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
fun start(context: Context, uri: Uri, sync: Boolean = false) {
|
fun start(context: Context, uri: Uri, sync: Boolean = false) {
|
||||||
val inputData = workDataOf(
|
val inputData = workDataOf(
|
||||||
LOCATION_URI_KEY to uri.toString(),
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
SYNC to sync,
|
SYNC_KEY to sync,
|
||||||
)
|
)
|
||||||
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
@ -85,5 +85,4 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private const val TAG = "BackupRestore"
|
private const val TAG = "BackupRestore"
|
||||||
|
|
||||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
|
private const val SYNC_KEY = "sync" // Boolean
|
||||||
private const val SYNC = "sync" // Boolean
|
|
||||||
|
@ -2,19 +2,38 @@ 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.domain.chapter.model.copyFrom
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
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.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.source.model.copyFrom
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
import eu.kanade.tachiyomi.util.BackupUtil
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.data.Manga_sync
|
||||||
|
import tachiyomi.data.Mangas
|
||||||
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.history.model.HistoryUpdate
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -24,19 +43,23 @@ import java.text.SimpleDateFormat
|
|||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupRestorer(
|
class BackupRestorer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val handler: DatabaseHandler = Injekt.get()
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||||
|
|
||||||
private var now = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchWindow = setFetchInterval.getWindow(now)
|
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
|
||||||
|
|
||||||
private var restoreAmount = 0
|
private var restoreAmount = 0
|
||||||
private var restoreProgress = 0
|
private var restoreProgress = 0
|
||||||
@ -92,7 +115,7 @@ class BackupRestorer(
|
|||||||
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
||||||
val backup = BackupUtil.decodeBackup(context, uri)
|
val backup = BackupUtil.decodeBackup(context, uri)
|
||||||
|
|
||||||
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
if (backup.backupCategories.isNotEmpty()) {
|
if (backup.backupCategories.isNotEmpty()) {
|
||||||
@ -103,9 +126,12 @@ class BackupRestorer(
|
|||||||
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||||
now = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentFetchWindow = setFetchInterval.getWindow(now)
|
currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
|
restoreAppPreferences(backup.backupPreferences)
|
||||||
|
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEach {
|
backup.backupManga.forEach {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
@ -115,12 +141,44 @@ class BackupRestorer(
|
|||||||
restoreManga(it, backup.backupCategories, sync)
|
restoreManga(it, backup.backupCategories, sync)
|
||||||
}
|
}
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
backupManager.restoreCategories(backupCategories)
|
// Get categories from file and from db
|
||||||
|
val dbCategories = getCategories.await()
|
||||||
|
|
||||||
|
val categories = backupCategories.map {
|
||||||
|
var category = it.getCategory()
|
||||||
|
var found = false
|
||||||
|
for (dbCategory in dbCategories) {
|
||||||
|
// If the category is already in the db, assign the id to the file's category
|
||||||
|
// and do nothing
|
||||||
|
if (category.name == dbCategory.name) {
|
||||||
|
category = category.copy(id = dbCategory.id)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
val id = handler.awaitOneExecutable {
|
||||||
|
categoriesQueries.insert(category.name, category.order, category.flags)
|
||||||
|
categoriesQueries.selectLastInsertedRowId()
|
||||||
|
}
|
||||||
|
category = category.copy(id = id)
|
||||||
|
}
|
||||||
|
|
||||||
|
category
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryPreferences.categorizedDisplaySettings().set(
|
||||||
|
(dbCategories + categories)
|
||||||
|
.distinctBy { it.flags }
|
||||||
|
.size > 1,
|
||||||
|
)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
|
||||||
@ -135,14 +193,14 @@ class BackupRestorer(
|
|||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
val dbManga = getMangaFromDatabase(manga.url, manga.source)
|
||||||
val restoredManga = if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else {
|
} else {
|
||||||
// Manga in database
|
// Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
val updatedManga = backupManager.restoreExistingManga(manga, dbManga)
|
val updatedManga = restoreExistingManga(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
@ -160,6 +218,50 @@ class BackupRestorer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
||||||
|
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||||
|
var updatedManga = manga.copy(id = dbManga._id)
|
||||||
|
updatedManga = updatedManga.copyFrom(dbManga)
|
||||||
|
updateManga(updatedManga)
|
||||||
|
return updatedManga
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateManga(manga: Manga): Long {
|
||||||
|
handler.await(true) {
|
||||||
|
mangasQueries.update(
|
||||||
|
source = manga.source,
|
||||||
|
url = manga.url,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.genre?.joinToString(separator = ", "),
|
||||||
|
title = manga.title,
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
lastUpdate = manga.lastUpdate,
|
||||||
|
nextUpdate = null,
|
||||||
|
calculateInterval = null,
|
||||||
|
initialized = manga.initialized,
|
||||||
|
viewer = manga.viewerFlags,
|
||||||
|
chapterFlags = manga.chapterFlags,
|
||||||
|
coverLastModified = manga.coverLastModified,
|
||||||
|
dateAdded = manga.dateAdded,
|
||||||
|
mangaId = manga.id,
|
||||||
|
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return manga.id
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
@ -175,12 +277,131 @@ class BackupRestorer(
|
|||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
): Manga {
|
): Manga {
|
||||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
val fetchedManga = restoreNewManga(manga)
|
||||||
backupManager.restoreChapters(fetchedManga, chapters)
|
restoreChapters(fetchedManga, chapters)
|
||||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
return fetchedManga
|
return fetchedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
|
||||||
|
|
||||||
|
val processed = chapters.map { chapter ->
|
||||||
|
var updatedChapter = chapter
|
||||||
|
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
||||||
|
if (dbChapter != null) {
|
||||||
|
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
||||||
|
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
||||||
|
if (dbChapter.read && !updatedChapter.read) {
|
||||||
|
updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
|
||||||
|
} else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
||||||
|
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
||||||
|
}
|
||||||
|
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
||||||
|
updatedChapter = updatedChapter.copy(bookmark = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedChapter.copy(mangaId = manga.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newChapters = processed.groupBy { it.id > 0 }
|
||||||
|
newChapters[true]?.let { updateKnownChapters(it) }
|
||||||
|
newChapters[false]?.let { insertChapters(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
private suspend fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
handler.await(true) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chaptersQueries.insert(
|
||||||
|
chapter.mangaId,
|
||||||
|
chapter.url,
|
||||||
|
chapter.name,
|
||||||
|
chapter.scanlator,
|
||||||
|
chapter.read,
|
||||||
|
chapter.bookmark,
|
||||||
|
chapter.lastPageRead,
|
||||||
|
chapter.chapterNumber,
|
||||||
|
chapter.sourceOrder,
|
||||||
|
chapter.dateFetch,
|
||||||
|
chapter.dateUpload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters with known database ids
|
||||||
|
*/
|
||||||
|
private suspend fun updateKnownChapters(chapters: List<Chapter>) {
|
||||||
|
handler.await(true) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chaptersQueries.update(
|
||||||
|
mangaId = null,
|
||||||
|
url = null,
|
||||||
|
name = null,
|
||||||
|
scanlator = null,
|
||||||
|
read = chapter.read,
|
||||||
|
bookmark = chapter.bookmark,
|
||||||
|
lastPageRead = chapter.lastPageRead,
|
||||||
|
chapterNumber = null,
|
||||||
|
sourceOrder = null,
|
||||||
|
dateFetch = null,
|
||||||
|
dateUpload = null,
|
||||||
|
chapterId = chapter.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return Updated manga info.
|
||||||
|
*/
|
||||||
|
private suspend fun restoreNewManga(manga: Manga): Manga {
|
||||||
|
return manga.copy(
|
||||||
|
initialized = manga.description != null,
|
||||||
|
id = insertManga(manga),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
private suspend fun insertManga(manga: Manga): Long {
|
||||||
|
return handler.awaitOneExecutable(true) {
|
||||||
|
mangasQueries.insert(
|
||||||
|
source = manga.source,
|
||||||
|
url = manga.url,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.genre,
|
||||||
|
title = manga.title,
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
lastUpdate = manga.lastUpdate,
|
||||||
|
nextUpdate = 0L,
|
||||||
|
calculateInterval = 0L,
|
||||||
|
initialized = manga.initialized,
|
||||||
|
viewerFlags = manga.viewerFlags,
|
||||||
|
chapterFlags = manga.chapterFlags,
|
||||||
|
coverLastModified = manga.coverLastModified,
|
||||||
|
dateAdded = manga.dateAdded,
|
||||||
|
updateStrategy = manga.updateStrategy,
|
||||||
|
)
|
||||||
|
mangasQueries.selectLastInsertedRowId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun restoreNewManga(
|
private suspend fun restoreNewManga(
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
@ -189,24 +410,240 @@ class BackupRestorer(
|
|||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
): Manga {
|
): Manga {
|
||||||
backupManager.restoreChapters(backupManga, chapters)
|
restoreChapters(backupManga, chapters)
|
||||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||||
return backupManga
|
return backupManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
backupManager.restoreCategories(manga, categories, backupCategories)
|
restoreCategories(manga, categories, backupCategories)
|
||||||
backupManager.restoreHistory(history)
|
restoreHistory(history)
|
||||||
backupManager.restoreTracking(manga, tracks)
|
restoreTracking(manga, tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to update dialog in [BackupConst]
|
* Restores the categories a manga is in.
|
||||||
*
|
*
|
||||||
* @param progress restore progress
|
* @param manga the manga whose categories have to be restored.
|
||||||
* @param amount total restoreAmount of manga
|
* @param categories the categories to restore.
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
*/
|
||||||
|
private suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||||
|
val dbCategories = getCategories.await()
|
||||||
|
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
||||||
|
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
backupCategories.firstOrNull {
|
||||||
|
it.order == backupCategoryOrder.toLong()
|
||||||
|
}?.let { backupCategory ->
|
||||||
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
|
dbCategory.name == backupCategory.name
|
||||||
|
}?.let { dbCategory ->
|
||||||
|
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||||
|
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||||
|
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
private suspend fun restoreHistory(history: List<BackupHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val toUpdate = mutableListOf<HistoryUpdate>()
|
||||||
|
for ((url, lastRead, readDuration) in history) {
|
||||||
|
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory = dbHistory.copy(
|
||||||
|
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
||||||
|
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||||
|
)
|
||||||
|
toUpdate.add(
|
||||||
|
HistoryUpdate(
|
||||||
|
chapterId = dbHistory.chapter_id,
|
||||||
|
readAt = dbHistory.last_read!!,
|
||||||
|
sessionReadDuration = dbHistory.time_read,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
handler
|
||||||
|
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
||||||
|
?.let {
|
||||||
|
toUpdate.add(
|
||||||
|
HistoryUpdate(
|
||||||
|
chapterId = it._id,
|
||||||
|
readAt = Date(lastRead),
|
||||||
|
sessionReadDuration = readDuration,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.await(true) {
|
||||||
|
toUpdate.forEach { payload ->
|
||||||
|
historyQueries.upsert(
|
||||||
|
payload.chapterId,
|
||||||
|
payload.readAt,
|
||||||
|
payload.sessionReadDuration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the sync of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose sync have to be restored.
|
||||||
|
* @param tracks the track list to restore.
|
||||||
|
*/
|
||||||
|
private suspend fun restoreTracking(manga: Manga, tracks: List<Track>) {
|
||||||
|
// Get tracks from database
|
||||||
|
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
||||||
|
val toUpdate = mutableListOf<Manga_sync>()
|
||||||
|
val toInsert = mutableListOf<Track>()
|
||||||
|
|
||||||
|
tracks
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
.map { it.copy(mangaId = manga.id) }
|
||||||
|
.forEach { track ->
|
||||||
|
var isInDatabase = false
|
||||||
|
for (dbTrack in dbTracks) {
|
||||||
|
if (track.syncId == dbTrack.sync_id) {
|
||||||
|
// The sync is already in the db, only update its fields
|
||||||
|
var temp = dbTrack
|
||||||
|
if (track.remoteId != dbTrack.remote_id) {
|
||||||
|
temp = temp.copy(remote_id = track.remoteId)
|
||||||
|
}
|
||||||
|
if (track.libraryId != dbTrack.library_id) {
|
||||||
|
temp = temp.copy(library_id = track.libraryId)
|
||||||
|
}
|
||||||
|
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
||||||
|
isInDatabase = true
|
||||||
|
toUpdate.add(temp)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
toInsert.add(track.copy(id = 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (toUpdate.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
toUpdate.forEach { track ->
|
||||||
|
manga_syncQueries.update(
|
||||||
|
track.manga_id,
|
||||||
|
track.sync_id,
|
||||||
|
track.remote_id,
|
||||||
|
track.library_id,
|
||||||
|
track.title,
|
||||||
|
track.last_chapter_read,
|
||||||
|
track.total_chapters,
|
||||||
|
track.status,
|
||||||
|
track.score,
|
||||||
|
track.remote_url,
|
||||||
|
track.start_date,
|
||||||
|
track.finish_date,
|
||||||
|
track._id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toInsert.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
toInsert.forEach { track ->
|
||||||
|
manga_syncQueries.insert(
|
||||||
|
track.mangaId,
|
||||||
|
track.syncId,
|
||||||
|
track.remoteId,
|
||||||
|
track.libraryId,
|
||||||
|
track.title,
|
||||||
|
track.lastChapterRead,
|
||||||
|
track.totalChapters,
|
||||||
|
track.status,
|
||||||
|
track.score,
|
||||||
|
track.remoteUrl,
|
||||||
|
track.startDate,
|
||||||
|
track.finishDate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
||||||
|
restorePreferences(preferences, preferenceStore)
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
||||||
|
preferences.forEach {
|
||||||
|
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||||
|
restorePreferences(it.prefs, sourcePrefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restorePreferences(
|
||||||
|
toRestore: List<BackupPreference>,
|
||||||
|
preferenceStore: PreferenceStore,
|
||||||
|
) {
|
||||||
|
val prefs = preferenceStore.getAll()
|
||||||
|
toRestore.forEach { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is IntPreferenceValue -> {
|
||||||
|
if (prefs[key] is Int?) {
|
||||||
|
preferenceStore.getInt(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LongPreferenceValue -> {
|
||||||
|
if (prefs[key] is Long?) {
|
||||||
|
preferenceStore.getLong(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FloatPreferenceValue -> {
|
||||||
|
if (prefs[key] is Float?) {
|
||||||
|
preferenceStore.getFloat(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StringPreferenceValue -> {
|
||||||
|
if (prefs[key] is String?) {
|
||||||
|
preferenceStore.getString(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is BooleanPreferenceValue -> {
|
||||||
|
if (prefs[key] is Boolean?) {
|
||||||
|
preferenceStore.getBoolean(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StringSetPreferenceValue -> {
|
||||||
|
if (prefs[key] is Set<*>?) {
|
||||||
|
preferenceStore.getStringSet(key).set(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) {
|
private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) {
|
||||||
notifier.showRestoreProgress(title, contentTitle, progress, amount)
|
notifier.showRestoreProgress(title, contentTitle, progress, amount)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -10,15 +11,18 @@ import java.util.Locale
|
|||||||
data class Backup(
|
data class Backup(
|
||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
|
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getBackupFilename(): String {
|
val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex()
|
||||||
|
|
||||||
|
fun getFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.proto.gz"
|
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ class BackupCategory(
|
|||||||
@ProtoNumber(1) var name: String,
|
@ProtoNumber(1) var name: String,
|
||||||
@ProtoNumber(2) var order: Long = 0,
|
@ProtoNumber(2) var order: Long = 0,
|
||||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var flags: Long = 0,
|
@ProtoNumber(100) var flags: Long = 0,
|
||||||
) {
|
) {
|
||||||
fun getCategory(): Category {
|
fun getCategory(): Category {
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupPreference(
|
||||||
|
@ProtoNumber(1) val key: String,
|
||||||
|
@ProtoNumber(2) val value: PreferenceValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSourcePreferences(
|
||||||
|
@ProtoNumber(1) val sourceKey: String,
|
||||||
|
@ProtoNumber(2) val prefs: List<BackupPreference>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class PreferenceValue
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class IntPreferenceValue(val value: Int) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongPreferenceValue(val value: Long) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FloatPreferenceValue(val value: Float) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StringPreferenceValue(val value: String) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StringSetPreferenceValue(val value: Set<String>) : PreferenceValue()
|
@ -8,9 +8,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -97,6 +99,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
editor.commit()
|
editor.commit()
|
||||||
editor.abortUnlessCommitted()
|
editor.abortUnlessCommitted()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Failed to put page list to cache" }
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} finally {
|
} finally {
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
@ -174,7 +177,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
* @return status of deletion for the file.
|
* @return status of deletion for the file.
|
||||||
*/
|
*/
|
||||||
private fun removeFileFromCache(file: String): Boolean {
|
private fun removeFileFromCache(file: String): Boolean {
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
// Make sure we don't delete the journal file (keeps track of cache)
|
||||||
if (file == "journal" || file.startsWith("journal.")) {
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -182,9 +185,10 @@ class ChapterCache(private val context: Context) {
|
|||||||
return try {
|
return try {
|
||||||
// Remove the extension from the file to get the key of the cache
|
// Remove the extension from the file to get the key of the cache
|
||||||
val key = file.substringBeforeLast(".")
|
val key = file.substringBeforeLast(".")
|
||||||
// Remove file from cache.
|
// Remove file from cache
|
||||||
diskCache.remove(key)
|
diskCache.remove(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ import nl.adaptivity.xmlutil.serialization.XML
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
@ -363,7 +362,7 @@ class Downloader(
|
|||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LOAD_PAGE
|
||||||
try {
|
try {
|
||||||
page.imageUrl = download.source.fetchImageUrl(page).awaitSingle()
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.ERROR
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,9 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
|
|||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.source.model.SourceNotInstalledException
|
import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||||
@ -90,7 +90,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private val getCategories: GetCategories = Injekt.get()
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||||
private val refreshTracks: RefreshTracks = Injekt.get()
|
private val refreshTracks: RefreshTracks = Injekt.get()
|
||||||
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
|
||||||
private val notifier = LibraryUpdateNotifier(context)
|
private val notifier = LibraryUpdateNotifier(context)
|
||||||
|
|
||||||
@ -186,7 +186,40 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
.distinctBy { it.manga.id }
|
.distinctBy { it.manga.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
||||||
|
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||||
|
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
|
.filter {
|
||||||
|
when {
|
||||||
|
it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
||||||
|
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
MANGA_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> {
|
||||||
|
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_completed))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
MANGA_HAS_UNREAD in restrictions && it.unreadCount != 0L -> {
|
||||||
|
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_caught_up))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
MANGA_NON_READ in restrictions && it.totalChapters > 0L && !it.hasStarted -> {
|
||||||
|
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_started))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> {
|
||||||
|
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
.sortedBy { it.manga.title }
|
.sortedBy { it.manga.title }
|
||||||
|
|
||||||
// Warn when excessively checking a single source
|
// Warn when excessively checking a single source
|
||||||
@ -197,6 +230,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
notifier.showQueueSizeWarningNotification()
|
notifier.showQueueSizeWarningNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
|
// TODO: surface skipped reasons to user?
|
||||||
|
logcat {
|
||||||
|
skippedUpdates
|
||||||
|
.groupBy { it.second }
|
||||||
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
|
.joinToString()
|
||||||
|
}
|
||||||
|
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,11 +256,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
||||||
val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now())
|
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
@ -237,23 +279,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
progressCount,
|
progressCount,
|
||||||
manga,
|
manga,
|
||||||
) {
|
) {
|
||||||
when {
|
|
||||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
|
||||||
|
|
||||||
MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
|
||||||
|
|
||||||
MANGA_HAS_UNREAD in restrictions && libraryManga.unreadCount != 0L ->
|
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_caught_up))
|
|
||||||
|
|
||||||
MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
|
||||||
|
|
||||||
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
|
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
try {
|
try {
|
||||||
val newChapters = updateManga(manga, fetchWindow)
|
val newChapters = updateManga(manga, fetchWindow)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
@ -279,8 +304,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libraryPreferences.autoUpdateTrackers().get()) {
|
if (libraryPreferences.autoUpdateTrackers().get()) {
|
||||||
refreshTracks(manga.id)
|
refreshTracks(manga.id)
|
||||||
@ -309,16 +332,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
errorFile.getUriCompat(context),
|
errorFile.getUriCompat(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (skippedUpdates.isNotEmpty()) {
|
|
||||||
// TODO: surface skipped reasons to user
|
|
||||||
logcat {
|
|
||||||
skippedUpdates
|
|
||||||
.groupBy { it.second }
|
|
||||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
|
||||||
.joinToString()
|
|
||||||
}
|
|
||||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
@ -428,8 +441,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
completed: AtomicInteger,
|
completed: AtomicInteger,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
block: suspend () -> Unit,
|
block: suspend () -> Unit,
|
||||||
) {
|
) = coroutineScope {
|
||||||
coroutineScope {
|
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
updatingManga.add(manga)
|
updatingManga.add(manga)
|
||||||
@ -451,7 +463,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes basic file of update errors to cache dir.
|
* Writes basic file of update errors to cache dir.
|
||||||
@ -497,7 +508,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
||||||
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
||||||
|
|
||||||
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
|
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/docs/guides/troubleshooting/"
|
||||||
|
|
||||||
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
|
||||||
|
@ -30,10 +30,14 @@ import tachiyomi.core.util.lang.launchUI
|
|||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.NumberFormat
|
||||||
|
|
||||||
class LibraryUpdateNotifier(private val context: Context) {
|
class LibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val preferences: SecurityPreferences by injectLazy()
|
private val preferences: SecurityPreferences by injectLazy()
|
||||||
|
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||||
|
maximumFractionDigits = 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
@ -78,7 +82,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
} else {
|
} else {
|
||||||
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
.setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total)))
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,11 +333,11 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
const val HELP_WARNING_URL = "https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.saver
|
package eu.kanade.tachiyomi.data.saver
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@ -28,30 +27,59 @@ class ImageSaver(
|
|||||||
val context: Context,
|
val context: Context,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
fun save(image: Image): Uri {
|
fun save(image: Image): Uri {
|
||||||
val data = image.data
|
val data = image.data
|
||||||
|
|
||||||
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
|
val type = ImageUtil.findImageType(data) ?: throw IllegalArgumentException("Not an image")
|
||||||
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
||||||
return save(data(), image.location.directory(context), filename)
|
return save(data(), image.location.directory(context), filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return saveApi29(image, type, filename, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||||
|
directory.mkdirs()
|
||||||
|
|
||||||
|
val destFile = File(directory, filename)
|
||||||
|
|
||||||
|
inputStream.use { input ->
|
||||||
|
destFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskUtil.scanMedia(context, destFile.toUri())
|
||||||
|
|
||||||
|
return destFile.getUriCompat(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun saveApi29(
|
||||||
|
image: Image,
|
||||||
|
type: ImageUtil.ImageType,
|
||||||
|
filename: String,
|
||||||
|
data: () -> InputStream,
|
||||||
|
): Uri {
|
||||||
val pictureDir =
|
val pictureDir =
|
||||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
|
||||||
val folderRelativePath = "${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/"
|
|
||||||
val imageLocation = (image.location as Location.Pictures).relativePath
|
val imageLocation = (image.location as Location.Pictures).relativePath
|
||||||
|
val relativePath = listOf(
|
||||||
|
Environment.DIRECTORY_PICTURES,
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
imageLocation,
|
||||||
|
).joinToString(File.separator)
|
||||||
|
|
||||||
val contentValues = contentValuesOf(
|
val contentValues = contentValuesOf(
|
||||||
|
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||||
MediaStore.Images.Media.RELATIVE_PATH to folderRelativePath + imageLocation,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val picture = findUriOrDefault(folderRelativePath, "$imageLocation$filename") {
|
val picture = findUriOrDefault(relativePath, filename) {
|
||||||
context.contentResolver.insert(
|
context.contentResolver.insert(
|
||||||
pictureDir,
|
pictureDir,
|
||||||
contentValues,
|
contentValues,
|
||||||
@ -74,49 +102,34 @@ class ImageSaver(
|
|||||||
return picture
|
return picture
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
|
||||||
|
|
||||||
inputStream.use { input ->
|
|
||||||
destFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, destFile.toUri())
|
|
||||||
|
|
||||||
return destFile.getUriCompat(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun findUriOrDefault(relativePath: String, imagePath: String, default: () -> Uri): Uri {
|
private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri {
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
MediaStore.MediaColumns._ID,
|
MediaStore.MediaColumns._ID,
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.MIME_TYPE,
|
|
||||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
||||||
|
|
||||||
|
// Need to make sure it ends with the separator
|
||||||
|
val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}"
|
||||||
|
|
||||||
context.contentResolver.query(
|
context.contentResolver.query(
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selection,
|
selection,
|
||||||
arrayOf(relativePath, imagePath),
|
arrayOf(normalizedPath, filename),
|
||||||
null,
|
null,
|
||||||
).use { cursor ->
|
).use { cursor ->
|
||||||
if (cursor != null && cursor.count >= 1) {
|
if (cursor != null && cursor.count >= 1) {
|
||||||
cursor.moveToFirst().let {
|
if (cursor.moveToFirst()) {
|
||||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||||
|
|
||||||
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return default()
|
return default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,19 +166,12 @@ sealed class Image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Location {
|
sealed interface Location {
|
||||||
data class Pictures private constructor(val relativePath: String) : Location {
|
data class Pictures(val relativePath: String) : Location
|
||||||
companion object {
|
|
||||||
fun create(relativePath: String = ""): Pictures {
|
|
||||||
return Pictures(relativePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Cache : Location
|
data object Cache : Location
|
||||||
|
|
||||||
fun directory(context: Context): File {
|
fun directory(context: Context): File {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
Cache -> context.cacheImageDir
|
|
||||||
is Pictures -> {
|
is Pictures -> {
|
||||||
val file = File(
|
val file = File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||||
@ -179,6 +185,7 @@ sealed interface Location {
|
|||||||
}
|
}
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
Cache -> context.cacheImageDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For track services api that support deleting a manga entry for a user's list
|
* Tracker that support deleting am entry from a user's list.
|
||||||
*/
|
*/
|
||||||
interface DeletableTrackService {
|
interface DeletableTracker {
|
||||||
|
|
||||||
suspend fun delete(track: Track): Track
|
suspend fun delete(track: Track): Track
|
||||||
}
|
}
|
@ -6,31 +6,32 @@ import tachiyomi.domain.manga.model.Manga
|
|||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
* A tracker that will never prompt the user to manually bind an entry.
|
||||||
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
* It is expected that such tracker can only work with specific sources and unique IDs.
|
||||||
*/
|
*/
|
||||||
interface EnhancedTrackService {
|
interface EnhancedTracker {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
* This tracker will only work with the sources that are accepted by this filter function.
|
||||||
*/
|
*/
|
||||||
fun accept(source: Source): Boolean {
|
fun accept(source: Source): Boolean {
|
||||||
return source::class.qualifiedName in getAcceptedSources()
|
return source::class.qualifiedName in getAcceptedSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully qualified source classes that this track service is compatible with.
|
* Fully qualified source classes that this tracker is compatible with.
|
||||||
*/
|
*/
|
||||||
fun getAcceptedSources(): List<String>
|
fun getAcceptedSources(): List<String>
|
||||||
|
|
||||||
fun loginNoop()
|
fun loginNoop()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* match is similar to TrackService.search, but only return zero or one match.
|
* Similar to [Tracker].search, but only returns zero or one match.
|
||||||
*/
|
*/
|
||||||
suspend fun match(manga: Manga): TrackSearch?
|
suspend fun match(manga: Manga): TrackSearch?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the provided source/track/manga triplet is from this TrackService
|
* Checks whether the provided source/track/manga triplet is from this [Tracker]
|
||||||
*/
|
*/
|
||||||
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
|
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
|
||||||
|
|
@ -5,7 +5,7 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
@ -28,7 +28,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
abstract class TrackService(val id: Long, val name: String) {
|
abstract class Tracker(val id: Long, val name: String) {
|
||||||
|
|
||||||
val trackPreferences: TrackPreferences by injectLazy()
|
val trackPreferences: TrackPreferences by injectLazy()
|
||||||
val networkService: NetworkHelper by injectLazy()
|
val networkService: NetworkHelper by injectLazy()
|
||||||
@ -83,7 +83,7 @@ abstract class TrackService(val id: Long, val name: String) {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun logout() {
|
open fun logout() {
|
||||||
trackPreferences.setTrackCredentials(this, "", "")
|
trackPreferences.setCredentials(this, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
open val isLoggedIn: Boolean
|
open val isLoggedIn: Boolean
|
||||||
@ -95,7 +95,7 @@ abstract class TrackService(val id: Long, val name: String) {
|
|||||||
fun getPassword() = trackPreferences.trackPassword(this).get()
|
fun getPassword() = trackPreferences.trackPassword(this).get()
|
||||||
|
|
||||||
fun saveCredentials(username: String, password: String) {
|
fun saveCredentials(username: String, password: String) {
|
||||||
trackPreferences.setTrackCredentials(this, username, password)
|
trackPreferences.setCredentials(this, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this to an interactor, and update all trackers based on common data
|
// TODO: move this to an interactor, and update all trackers based on common data
|
||||||
@ -111,7 +111,7 @@ abstract class TrackService(val id: Long, val name: String) {
|
|||||||
|
|
||||||
insertTrack.await(track)
|
insertTrack.await(track)
|
||||||
|
|
||||||
// TODO: merge into SyncChaptersWithTrackServiceTwoWay?
|
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||||
// Update chapter progress if newer chapters marked read locally
|
// Update chapter progress if newer chapters marked read locally
|
||||||
if (hasReadChapters) {
|
if (hasReadChapters) {
|
||||||
val latestLocalReadChapterNumber = allChapters
|
val latestLocalReadChapterNumber = allChapters
|
||||||
@ -143,7 +143,7 @@ abstract class TrackService(val id: Long, val name: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncChapterProgressWithTrack.await(mangaId, track, this@TrackService)
|
syncChapterProgressWithTrack.await(mangaId, track, this@Tracker)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
withUIContext { Injekt.get<Application>().toast(e.message) }
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kavita.Kavita
|
import eu.kanade.tachiyomi.data.track.kavita.Kavita
|
||||||
@ -11,33 +10,27 @@ 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
|
||||||
|
|
||||||
class TrackManager(context: Context) {
|
class TrackerManager {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1L
|
|
||||||
const val ANILIST = 2L
|
const val ANILIST = 2L
|
||||||
const val KITSU = 3L
|
const val KITSU = 3L
|
||||||
const val SHIKIMORI = 4L
|
|
||||||
const val BANGUMI = 5L
|
|
||||||
const val KOMGA = 6L
|
|
||||||
const val MANGA_UPDATES = 7L
|
|
||||||
const val KAVITA = 8L
|
const val KAVITA = 8L
|
||||||
const val SUWAYOMI = 9L
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(MYANIMELIST)
|
val myAnimeList = MyAnimeList(1L)
|
||||||
val aniList = Anilist(ANILIST)
|
val aniList = Anilist(ANILIST)
|
||||||
val kitsu = Kitsu(KITSU)
|
val kitsu = Kitsu(KITSU)
|
||||||
val shikimori = Shikimori(SHIKIMORI)
|
val shikimori = Shikimori(4L)
|
||||||
val bangumi = Bangumi(BANGUMI)
|
val bangumi = Bangumi(5L)
|
||||||
val komga = Komga(KOMGA)
|
val komga = Komga(6L)
|
||||||
val mangaUpdates = MangaUpdates(MANGA_UPDATES)
|
val mangaUpdates = MangaUpdates(7L)
|
||||||
val kavita = Kavita(context, KAVITA)
|
val kavita = Kavita(KAVITA)
|
||||||
val suwayomi = Suwayomi(SUWAYOMI)
|
val suwayomi = Suwayomi(9L)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
|
val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
|
||||||
|
|
||||||
fun getService(id: Long) = services.find { it.id == id }
|
fun get(id: Long) = trackers.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLoggedIn }
|
fun hasLoggedIn() = trackers.any { it.isLoggedIn }
|
||||||
}
|
}
|
@ -4,15 +4,15 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTrackService
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
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.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Anilist(id: Long) : TrackService(id, "AniList"), DeletableTrackService {
|
class Anilist(id: Long) : Tracker(id, "AniList"), DeletableTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
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.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -20,7 +20,7 @@ data class ALManga(
|
|||||||
val total_chapters: Int,
|
val total_chapters: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||||
media_id = this@ALManga.media_id
|
media_id = this@ALManga.media_id
|
||||||
title = title_user_pref
|
title = title_user_pref
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
@ -50,7 +50,7 @@ data class ALUserManga(
|
|||||||
val manga: ALManga,
|
val manga: ALManga,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
||||||
media_id = manga.media_id
|
media_id = manga.media_id
|
||||||
title = manga.title_user_pref
|
title = manga.title_user_pref
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
@ -62,7 +62,7 @@ data class ALUserManga(
|
|||||||
total_chapters = manga.total_chapters
|
total_chapters = manga.total_chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = when (list_status) {
|
private fun toTrackStatus() = when (list_status) {
|
||||||
"CURRENT" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"PAUSED" -> Anilist.ON_HOLD
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
|
@ -4,19 +4,19 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
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.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Bangumi(id: Long) : TrackService(id, "Bangumi") {
|
class Bangumi(id: Long) : Tracker(id, "Bangumi") {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { BangumiInterceptor(this) }
|
private val interceptor by lazy { BangumiInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { BangumiApi(client, interceptor) }
|
private val api by lazy { BangumiApi(id, client, interceptor) }
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return IntRange(0, 10).map(Int::toString)
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.bangumi
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -26,7 +25,11 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
class BangumiApi(
|
||||||
|
private val trackId: Long,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
interceptor: BangumiInterceptor,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@ -105,7 +108,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
return TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
media_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||||
cover_url = coverUrl
|
cover_url = coverUrl
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kavita
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"), EnhancedTrackService {
|
class Kavita(id: Long) : Tracker(id, "Kavita"), EnhancedTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNREAD = 1
|
const val UNREAD = 1
|
||||||
@ -27,6 +29,8 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"
|
|||||||
private val interceptor by lazy { KavitaInterceptor(this) }
|
private val interceptor by lazy { KavitaInterceptor(this) }
|
||||||
val api by lazy { KavitaApi(client, interceptor) }
|
val api by lazy { KavitaApi(client, interceptor) }
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(74, 198, 148)
|
override fun getLogoColor() = Color.rgb(74, 198, 148)
|
||||||
@ -83,7 +87,7 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"
|
|||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackService.isLogged works by checking that credentials are saved.
|
// [Tracker].isLogged works by checking that credentials are saved.
|
||||||
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
||||||
override fun loginNoop() {
|
override fun loginNoop() {
|
||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
@ -110,28 +114,29 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"
|
|||||||
|
|
||||||
fun loadOAuth() {
|
fun loadOAuth() {
|
||||||
val oauth = OAuth()
|
val oauth = OAuth()
|
||||||
for (sourceId in 1..3) {
|
for (id in 1..3) {
|
||||||
val authentication = oauth.authentications[sourceId - 1]
|
val authentication = oauth.authentications[id - 1]
|
||||||
val sourceSuffixID by lazy {
|
val sourceId by lazy {
|
||||||
val key = "kavita_$sourceId/all/1" // Hardcoded versionID to 1
|
val key = "kavita_$id/all/1" // Hardcoded versionID to 1
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
|
||||||
.reduce(Long::or) and Long.MAX_VALUE
|
.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
val preferences: SharedPreferences by lazy {
|
val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences()
|
||||||
context.getSharedPreferences("source_$sourceSuffixID", 0x0000)
|
|
||||||
}
|
val prefApiUrl = preferences.getString("APIURL", "")
|
||||||
val prefApiUrl = preferences.getString("APIURL", "")!!
|
val prefApiKey = preferences.getString("APIKEY", "")
|
||||||
if (prefApiUrl.isEmpty()) {
|
if (prefApiUrl.isNullOrEmpty() || prefApiKey.isNullOrEmpty()) {
|
||||||
// Source not configured. Skip
|
// Source not configured. Skip
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val prefApiKey = preferences.getString("APIKEY", "")!!
|
|
||||||
val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey)
|
val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey)
|
||||||
if (token.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
// Source is not accessible. Skip
|
// Source is not accessible. Skip
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication.apiUrl = prefApiUrl
|
authentication.apiUrl = prefApiUrl
|
||||||
authentication.jwtToken = token.toString()
|
authentication.jwtToken = token.toString()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kavita
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ data class SeriesDto(
|
|||||||
val libraryId: Int,
|
val libraryId: Int,
|
||||||
val libraryName: String? = "",
|
val libraryName: String? = "",
|
||||||
) {
|
) {
|
||||||
fun toTrack(): TrackSearch = TrackSearch.create(TrackManager.KAVITA).also {
|
fun toTrack(): TrackSearch = TrackSearch.create(TrackerManager.KAVITA).also {
|
||||||
it.title = name
|
it.title = name
|
||||||
it.summary = ""
|
it.summary = ""
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,15 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTrackService
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
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.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
class Kitsu(id: Long) : TrackService(id, "Kitsu"), DeletableTrackService {
|
class Kitsu(id: Long) : Tracker(id, "Kitsu"), DeletableTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
|||||||
|
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -35,7 +35,7 @@ class KitsuSearchManga(obj: JsonObject) {
|
|||||||
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
|
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
media_id = this@KitsuSearchManga.id
|
media_id = this@KitsuSearchManga.id
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
@ -67,7 +67,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
||||||
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
media_id = libraryId
|
media_id = libraryId
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
|
@ -4,8 +4,8 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
@ -13,7 +13,7 @@ import okhttp3.OkHttpClient
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService {
|
class Komga(id: Long) : Tracker(id, "Komga"), EnhancedTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNREAD = 1
|
const val UNREAD = 1
|
||||||
@ -26,7 +26,7 @@ class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService {
|
|||||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val api by lazy { KomgaApi(client) }
|
val api by lazy { KomgaApi(id, client) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService {
|
|||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackService.isLogged works by checking that credentials are saved.
|
// [Tracker].isLogged works by checking that credentials are saved.
|
||||||
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
||||||
override fun loginNoop() {
|
override fun loginNoop() {
|
||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.komga
|
package eu.kanade.tachiyomi.data.track.komga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
@ -19,7 +18,10 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
|
|
||||||
private const val READLIST_API = "/api/v1/readlists"
|
private const val READLIST_API = "/api/v1/readlists"
|
||||||
|
|
||||||
class KomgaApi(private val client: OkHttpClient) {
|
class KomgaApi(
|
||||||
|
private val trackId: Long,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@ -85,13 +87,13 @@ class KomgaApi(private val client: OkHttpClient) {
|
|||||||
return getTrackSearch(track.tracking_url)
|
return getTrackSearch(track.tracking_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also {
|
||||||
it.title = metadata.title
|
it.title = metadata.title
|
||||||
it.summary = metadata.summary
|
it.summary = metadata.summary
|
||||||
it.publishing_status = metadata.status
|
it.publishing_status = metadata.status
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also {
|
||||||
it.title = name
|
it.title = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,13 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTrackService
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
||||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
|
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
|
||||||
class MangaUpdates(id: Long) : TrackService(id, "MangaUpdates"), DeletableTrackService {
|
class MangaUpdates(id: Long) : Tracker(id, "MangaUpdates"), DeletableTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING_LIST = 0
|
const val READING_LIST = 0
|
||||||
|
@ -4,14 +4,14 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTrackService
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
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.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class MyAnimeList(id: Long) : TrackService(id, "MyAnimeList"), DeletableTrackService {
|
class MyAnimeList(id: Long) : Tracker(id, "MyAnimeList"), DeletableTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
@ -28,7 +28,7 @@ class MyAnimeList(id: Long) : TrackService(id, "MyAnimeList"), DeletableTrackSer
|
|||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
|
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
|
||||||
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
private val api by lazy { MyAnimeListApi(id, client, interceptor) }
|
||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -32,7 +31,11 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
class MyAnimeListApi(
|
||||||
|
private val trackId: Long,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
interceptor: MyAnimeListInterceptor,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.parseAs<JsonObject>()
|
.parseAs<JsonObject>()
|
||||||
.let {
|
.let {
|
||||||
val obj = it.jsonObject
|
val obj = it.jsonObject
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
media_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
title = obj["title"]!!.jsonPrimitive.content
|
||||||
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
||||||
|
@ -4,14 +4,14 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTrackService
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
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.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Shikimori(id: Long) : TrackService(id, "Shikimori"), DeletableTrackService {
|
class Shikimori(id: Long) : Tracker(id, "Shikimori"), DeletableTracker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
@ -26,7 +26,7 @@ class Shikimori(id: Long) : TrackService(id, "Shikimori"), DeletableTrackService
|
|||||||
|
|
||||||
private val interceptor by lazy { ShikimoriInterceptor(this) }
|
private val interceptor by lazy { ShikimoriInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { ShikimoriApi(client, interceptor) }
|
private val api by lazy { ShikimoriApi(id, client, interceptor) }
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return IntRange(0, 10).map(Int::toString)
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
|||||||
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@ -28,7 +27,11 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
class ShikimoriApi(
|
||||||
|
private val trackId: Long,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
interceptor: ShikimoriInterceptor,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
return TrackSearch.create(TrackManager.SHIKIMORI).apply {
|
return TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
media_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["name"]!!.jsonPrimitive.content
|
title = obj["name"]!!.jsonPrimitive.content
|
||||||
total_chapters = obj["chapters"]!!.jsonPrimitive.int
|
total_chapters = obj["chapters"]!!.jsonPrimitive.int
|
||||||
@ -110,7 +113,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
||||||
return Track.create(TrackManager.SHIKIMORI).apply {
|
return Track.create(trackId).apply {
|
||||||
title = mangas["name"]!!.jsonPrimitive.content
|
title = mangas["name"]!!.jsonPrimitive.content
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
media_id = obj["id"]!!.jsonPrimitive.long
|
||||||
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
|
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
|
||||||
|
@ -4,16 +4,16 @@ import android.graphics.Color
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import tachiyomi.domain.manga.model.Manga as DomainManga
|
import tachiyomi.domain.manga.model.Manga as DomainManga
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Suwayomi(id: Long) : TrackService(id, "Suwayomi"), EnhancedTrackService {
|
class Suwayomi(id: Long) : Tracker(id, "Suwayomi"), EnhancedTracker {
|
||||||
|
|
||||||
val api by lazy { TachideskApi() }
|
val api by lazy { SuwayomiApi(id) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
@ -23,7 +23,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class TachideskApi {
|
class SuwayomiApi(private val trackId: Long) {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@ -61,7 +61,7 @@ class TachideskApi {
|
|||||||
.parseAs<MangaDataClass>()
|
.parseAs<MangaDataClass>()
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSearch.create(TrackManager.SUWAYOMI).apply {
|
TrackSearch.create(trackId).apply {
|
||||||
title = manga.title
|
title = manga.title
|
||||||
cover_url = "$url/thumbnail"
|
cover_url = "$url/thumbnail"
|
||||||
summary = manga.description.orEmpty()
|
summary = manga.description.orEmpty()
|
||||||
@ -100,26 +100,24 @@ class TachideskApi {
|
|||||||
return getTrackSearch(track.tracking_url)
|
return getTrackSearch(track.tracking_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tachideskExtensionId by lazy {
|
private val sourceId by lazy {
|
||||||
val key = "tachidesk/en/1"
|
val key = "tachidesk/en/1"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
||||||
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
|
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
|
||||||
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
|
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ADDRESS_TITLE = "Server URL Address"
|
|
||||||
private const val ADDRESS_DEFAULT = ""
|
|
||||||
private const val LOGIN_TITLE = "Login (Basic Auth)"
|
|
||||||
private const val LOGIN_DEFAULT = ""
|
|
||||||
private const val PASSWORD_TITLE = "Password (Basic Auth)"
|
|
||||||
private const val PASSWORD_DEFAULT = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ADDRESS_TITLE = "Server URL Address"
|
||||||
|
private const val ADDRESS_DEFAULT = ""
|
||||||
|
private const val LOGIN_TITLE = "Login (Basic Auth)"
|
||||||
|
private const val LOGIN_DEFAULT = ""
|
||||||
|
private const val PASSWORD_TITLE = "Password (Basic Auth)"
|
||||||
|
private const val PASSWORD_DEFAULT = ""
|
@ -143,7 +143,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
||||||
setContentText(context.getString(R.string.update_check_fdroid_migration_info))
|
setContentText(context.getString(R.string.update_check_fdroid_migration_info))
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version"))
|
setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds"))
|
||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
|
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.lang.use
|
import eu.kanade.tachiyomi.util.lang.use
|
||||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||||
@ -100,7 +101,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +264,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
isRegistered = true
|
isRegistered = true
|
||||||
|
|
||||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||||
context.registerReceiver(this, filter)
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,8 +10,6 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this.id)
|
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this.id)
|
||||||
|
|
||||||
fun Source.getPreferenceKey(): String = "source_$id"
|
|
||||||
|
|
||||||
fun Source.toStubSource(): StubSource = StubSource(id = id, lang = lang, name = name)
|
fun Source.toStubSource(): StubSource = StubSource(id = id, lang = lang, name = name)
|
||||||
|
|
||||||
fun Source.getNameForMangaInfo(): String {
|
fun Source.getNameForMangaInfo(): String {
|
||||||
|
@ -102,7 +102,7 @@ class ExtensionDetailsScreenModel(
|
|||||||
val extension = state.value.extension ?: return ""
|
val extension = state.value.extension ?: return ""
|
||||||
|
|
||||||
if (!extension.hasReadme) {
|
if (!extension.hasReadme) {
|
||||||
return "https://tachiyomi.org/help/faq/#extensions"
|
return "https://tachiyomi.org/docs/faq/browse/extensions"
|
||||||
}
|
}
|
||||||
|
|
||||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||||
|
@ -39,7 +39,7 @@ import eu.kanade.presentation.util.Screen
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
@ -134,12 +134,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
|||||||
|
|
||||||
private fun populateScreen(): PreferenceScreen {
|
private fun populateScreen(): PreferenceScreen {
|
||||||
val sourceId = requireArguments().getLong(SOURCE_ID)
|
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||||
val source = Injekt.get<SourceManager>().get(sourceId)!!
|
val source = Injekt.get<SourceManager>().get(sourceId)!! as ConfigurableSource
|
||||||
|
|
||||||
check(source is ConfigurableSource)
|
val dataStore = SharedPreferencesDataStore(source.sourcePreferences())
|
||||||
|
|
||||||
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
|
||||||
val dataStore = SharedPreferencesDataStore(sharedPreferences)
|
|
||||||
preferenceManager.preferenceDataStore = dataStore
|
preferenceManager.preferenceDataStore = dataStore
|
||||||
|
|
||||||
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||||
|
@ -35,8 +35,8 @@ import eu.kanade.domain.manga.model.toSManga
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
@ -177,7 +177,7 @@ internal class MigrateDialogScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val enhancedServices by lazy {
|
private val enhancedServices by lazy {
|
||||||
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>()
|
Injekt.get<TrackerManager>().trackers.filterIsInstance<EnhancedTracker>()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun migrateManga(
|
suspend fun migrateManga(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user