Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 0413d502c1 Revert Jdk 11 update 2021-02-12 20:05:46 -05:00
327 changed files with 4366 additions and 8418 deletions
+1 -3
View File
@@ -2,7 +2,7 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0) - I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions - I have updated all extensions
- 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
@@ -24,5 +24,3 @@ I acknowledge that:
## Other details ## Other details
Additional details and attachments. Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+1 -3
View File
@@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0) - I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions - I have updated all extensions
- 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
@@ -34,5 +34,3 @@ This happened instead.
## Other details ## Other details
Additional details and attachments. Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+1 -1
View File
@@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0) - I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions - I have updated all extensions
- 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
+28 -27
View File
@@ -7,30 +7,31 @@ jobs:
autoclose: autoclose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose issues - name: Autoclose when created in wrong repo
uses: arkon/issue-closer-action@v3.0 uses: arkon/issue-closer-action@v1.1
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: | type: title
[ regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
{ message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
"type": "title", - name: Autoclose when no short description provided
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*", uses: arkon/issue-closer-action@v1.1
"message": "It was not opened in the correct repo, as the template mentioned." with:
}, repo-token: ${{ secrets.GITHUB_TOKEN }}
{ type: title
"type": "title", regex: ".*<Write short description here>*"
"regex": ".*<Write short description here>*", message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
"message": "The description in the title was not filled out." - name: Autoclose when body acknowledgement section not removed
}, uses: arkon/issue-closer-action@v1.1
{ with:
"type": "body", repo-token: ${{ secrets.GITHUB_TOKEN }}
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", type: body
"message": "The acknowledgment section was not removed." regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
}, message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
{ - name: Autoclose when body requested information not filled out
"type": "body", uses: arkon/issue-closer-action@v1.1
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*", with:
"message": "Requested information in the template was not filled out." repo-token: ${{ secrets.GITHUB_TOKEN }}
} type: body
] regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
-19
View File
@@ -1,19 +0,0 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 * * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '2'
pr-lock-inactive-days: '2'
-76
View File
@@ -1,76 +0,0 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+2 -7
View File
@@ -1,6 +1,6 @@
| Preview Builds | Release Builds | Tachiyomi Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) | | [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
@@ -109,12 +109,7 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
<details><summary>Contributing</summary> <details><summary>Contributing</summary>
See [CONTRIBUTING.md](./CONTRIBUTING.md). See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
</details>
<details><summary>Code of Conduct</summary>
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
</details> </details>
## FAQ ## FAQ
+27 -25
View File
@@ -34,8 +34,8 @@ android {
minSdkVersion(AndroidConfig.minSdk) minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk) targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 14 versionCode = 13
versionName = "1.6.0" versionName = "1.5.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -95,7 +95,6 @@ android {
exclude("META-INF/LICENSE") exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt") exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE") exclude("META-INF/NOTICE")
exclude("META-INF/*.kotlin_module")
// Compatibility for two RxJava versions (EXH) // Compatibility for two RxJava versions (EXH)
exclude("META-INF/rxjava.properties") exclude("META-INF/rxjava.properties")
@@ -127,20 +126,20 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1") implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries // AndroidX libraries
implementation("androidx.annotation:annotation:1.3.0-alpha01") implementation("androidx.annotation:annotation:1.2.0-beta01")
implementation("androidx.appcompat:appcompat:1.3.0-rc01") implementation("androidx.appcompat:appcompat:1.3.0-beta01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03") implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01") implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0") implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.3.2") implementation("androidx.core:core-ktx:1.5.0-beta01")
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0") implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.3.0" val lifecycleVersion = "2.3.0-rc01"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@@ -151,7 +150,7 @@ dependencies {
// UI library // UI library
implementation("com.google.android.material:material:1.3.0") implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.3") "standardImplementation"("com.google.firebase:firebase-core:18.0.2")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxandroid:1.2.1") implementation("io.reactivex:rxandroid:1.2.1")
@@ -160,7 +159,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0") implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
val okhttpVersion = "5.0.0-alpha.2" val okhttpVersion = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -194,7 +193,7 @@ dependencies {
implementation("io.requery:sqlite-android:3.33.0") implementation("io.requery:sqlite-android:3.33.0")
// Preferences // Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4") implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
// Model View Presenter // Model View Presenter
val nucleusVersion = "3.0.0" val nucleusVersion = "3.0.0"
@@ -205,12 +204,14 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val glideVersion = "4.12.0" val glideVersion = "4.11.0"
implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion") kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0") implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
// Logging // Logging
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
@@ -228,8 +229,7 @@ dependencies {
implementation("eu.davidea:flexible-adapter-ui:1.0.0") implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices // 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1" val materialDialogsVersion = "3.1.1"
@@ -242,7 +242,7 @@ dependencies {
implementation("com.bluelinelabs:conductor-support:2.1.5") { implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support") exclude(group = "com.android.support")
} }
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1") implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
// FlowBinding // FlowBinding
val flowbindingVersion = "0.12.0" val flowbindingVersion = "0.12.0"
@@ -256,7 +256,7 @@ dependencies {
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}") implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests // Tests
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.1")
testImplementation("org.assertj:assertj-core:3.16.1") testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19") testImplementation("org.mockito:mockito-core:1.10.19")
@@ -285,11 +285,11 @@ dependencies {
implementation ("info.debatty:java-string-similarity:2.0.0") implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH) // Firebase (EH)
implementation("com.google.firebase:firebase-analytics-ktx:18.0.3") implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1") implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
// Better logging (EH) // Better logging (EH)
implementation("com.elvishew:xlog:1.9.0") implementation("com.elvishew:xlog:1.7.1")
// Debug utils (EH) // Debug utils (EH)
val debugOverlayVersion = "1.1.3" val debugOverlayVersion = "1.1.3"
@@ -302,10 +302,12 @@ dependencies {
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0") implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
// JsonReader for similar manga // JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.12.0") implementation("com.squareup.moshi:moshi:1.11.0")
implementation("com.mikepenz:fastadapter:5.4.0") implementation("androidx.gridlayout:gridlayout:1.0.0")
// SY <--
implementation("com.mikepenz:fastadapter:5.3.4")
// SY -->
} }
tasks { tasks {
+2 -7
View File
@@ -14,7 +14,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- For managing extensions --> <!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -150,10 +149,6 @@
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="exh.ui.login.EhLoginActivity"
android:label="EHentaiLogin" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -320,7 +315,7 @@
android:scheme="https" /> android:scheme="https" />
<!-- MangaDex --> <!-- MangaDex -->
<!--<data <data
android:scheme="https" android:scheme="https"
android:host="www.mangadex.org" android:host="www.mangadex.org"
android:pathPrefix="/manga/" /> android:pathPrefix="/manga/" />
@@ -369,7 +364,7 @@
<data <data
android:scheme="https" android:scheme="https"
android:host="www.mangadex.cc" android:host="www.mangadex.cc"
android:pathPrefix="/chapter/" />--> android:pathPrefix="/chapter/" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
+51 -21
View File
@@ -22,6 +22,7 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
@@ -35,11 +36,11 @@ import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
import exh.log.EHLogLevel import exh.log.EHLogLevel
import exh.log.EnhancedFilePrinter import exh.log.EnhancedFilePrinter
import exh.log.XLogTree
import exh.log.xLogD
import exh.log.xLogE
import exh.syDebugVersion import exh.syDebugVersion
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -50,6 +51,7 @@ import java.security.Security
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import kotlin.concurrent.thread
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.days import kotlin.time.days
@@ -57,11 +59,12 @@ open class App : Application(), LifecycleObserver {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private lateinit var firebaseAnalytics: FirebaseAnalytics
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupExhLogging() // EXH logging setupExhLogging() // EXH logging
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
if (!BuildConfig.DEBUG) addAnalytics() if (!BuildConfig.DEBUG) addAnalytics()
workaroundAndroid7BrokenSSL() workaroundAndroid7BrokenSSL()
@@ -75,6 +78,7 @@ open class App : Application(), LifecycleObserver {
setupNotificationChannels() setupNotificationChannels()
Realm.init(this) Realm.init(this)
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
setupDebugOverlay() setupDebugOverlay()
} }
@@ -101,22 +105,23 @@ open class App : Application(), LifecycleObserver {
try { try {
SSLContext.getInstance("TLSv1.2") SSLContext.getInstance("TLSv1.2")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
xLogE("Could not install Android 7 broken SSL workaround!", e) XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
} }
try { try {
ProviderInstaller.installIfNeeded(applicationContext) ProviderInstaller.installIfNeeded(applicationContext)
} catch (e: GooglePlayServicesRepairableException) { } catch (e: GooglePlayServicesRepairableException) {
xLogE("Could not install Android 7 broken SSL workaround!", e) XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
} catch (e: GooglePlayServicesNotAvailableException) { } catch (e: GooglePlayServicesNotAvailableException) {
xLogE("Could not install Android 7 broken SSL workaround!", e) XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
} }
} }
} }
private fun addAnalytics() { private fun addAnalytics() {
firebaseAnalytics = Firebase.analytics
if (syDebugVersion != "0") { if (syDebugVersion != "0") {
Firebase.analytics.setUserProperty("preview_version", syDebugVersion) firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
} }
} }
@@ -132,13 +137,36 @@ open class App : Application(), LifecycleObserver {
Notifications.createChannels(this) Notifications.createChannels(this)
} }
// EXH
private fun deleteOldMetadataRealm() {
val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm")
.schemaVersion(3)
.deleteRealmIfMigrationNeeded()
.build()
Realm.deleteRealm(config)
// Delete old paper db files
listOf(
File(filesDir, "gallery-ex"),
File(filesDir, "gallery-perveden"),
File(filesDir, "gallery-nhentai")
).forEach {
if (it.exists()) {
thread {
it.deleteRecursively()
}
}
}
}
// EXH // EXH
private fun setupExhLogging() { private fun setupExhLogging() {
EHLogLevel.init(this) EHLogLevel.init(this)
val logLevel = when { val logLevel = when {
EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG BuildConfig.DEBUG -> LogLevel.DEBUG
else -> LogLevel.WARN else -> LogLevel.WARN
} }
@@ -160,8 +188,9 @@ open class App : Application(), LifecycleObserver {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
printers += EnhancedFilePrinter printers += EnhancedFilePrinter
.Builder(logFolder.absolutePath) { .Builder(logFolder.absolutePath)
fileNameGenerator = object : DateFileNameGenerator() { .fileNameGenerator(
object : DateFileNameGenerator() {
override fun generateFileName(logLevel: Int, timestamp: Long): String { override fun generateFileName(logLevel: Int, timestamp: Long): String {
return super.generateFileName( return super.generateFileName(
logLevel, logLevel,
@@ -169,12 +198,13 @@ open class App : Application(), LifecycleObserver {
) + "-${BuildConfig.BUILD_TYPE}.log" ) + "-${BuildConfig.BUILD_TYPE}.log"
} }
} }
flattener { timeMillis, level, tag, message -> )
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message" .flattener { timeMillis, level, tag, message ->
} "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
backupStrategy = NeverBackupStrategy()
} }
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
.backupStrategy(NeverBackupStrategy())
.build()
// Install Crashlytics in prod // Install Crashlytics in prod
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
@@ -186,8 +216,8 @@ open class App : Application(), LifecycleObserver {
*printers.toTypedArray() *printers.toTypedArray()
) )
xLogD("Application booting...") XLog.tag("Init").d("Application booting...")
xLogD( XLog.tag("Init").disableStackTrace().d(
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" + "App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
"Preview build: $syDebugVersion\n" + "Preview build: $syDebugVersion\n" +
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" + "Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
@@ -212,7 +242,7 @@ open class App : Application(), LifecycleObserver {
.install() .install()
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// Crashes if app is in background // Crashes if app is in background
xLogE("Failed to initialize debug overlay, app in background?", e) XLog.tag("Init").e("Failed to initialize debug overlay, app in background?", e)
} }
} }
} }
@@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@@ -130,17 +129,6 @@ object Migrations {
context.toast(R.string.myanimelist_relogin) context.toast(R.string.myanimelist_relogin)
} }
} }
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
return true return true
} }
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@@ -24,10 +23,6 @@ abstract class AbstractBackupManager(protected val context: Context) {
internal val trackManager: TrackManager by injectLazy() internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy() protected val preferences: PreferencesHelper by injectLazy()
// SY -->
protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <--
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
/** /**
@@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
@@ -25,10 +24,6 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
protected val db: DatabaseHelper by injectLazy() protected val db: DatabaseHelper by injectLazy()
protected val trackManager: TrackManager by injectLazy() protected val trackManager: TrackManager by injectLazy()
// SY -->
protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <--
var job: Job? = null var job: Job? = null
protected lateinit var backupManager: T protected lateinit var backupManager: T
@@ -30,12 +30,7 @@ class BackupCreateService : Service() {
internal const val BACKUP_HISTORY_MASK = 0x4 internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8 internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8 internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
// SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_ALL = 0x1F
// SY <--
/** /**
* Returns the status of the service. * Returns the status of the service.
@@ -24,7 +24,6 @@ class BackupNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false) setAutoCancel(false)
setOngoing(true) setOngoing(true)
setOnlyAlertOnce(true)
} }
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) { private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
@@ -42,6 +41,7 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.creating_backup)) setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true) setProgress(0, 0, true)
setOnlyAlertOnce(true)
} }
builder.show(Notifications.ID_BACKUP_PROGRESS) builder.show(Notifications.ID_BACKUP_PROGRESS)
@@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_show_errors), context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri) NotificationReceiver.openErrorLogPendingActivity(context, uri)
) )
} }
@@ -43,11 +43,12 @@ class BackupRestoreService : Service() {
* @param context context of application * @param context context of application
* @param uri path of Uri * @param uri path of Uri
*/ */
fun start(context: Context, uri: Uri, mode: Int) { fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply { val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_MODE, mode) putExtra(BackupConst.EXTRA_MODE, mode)
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
@@ -118,12 +119,13 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL) val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
// Cancel any previous job if needed. // Cancel any previous job if needed.
backupRestore?.job?.cancel() backupRestore?.job?.cancel()
backupRestore = when (mode) { backupRestore = when (mode) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier) BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
else -> LegacyBackupRestore(this, notifier) else -> LegacyBackupRestore(this, notifier)
} }
@@ -8,8 +8,6 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATE
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
@@ -31,8 +29,11 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.source.online.all.MergedSource
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.JsonSavedSearch
@@ -163,7 +164,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/ */
private fun backupMangaObject(manga: Manga, options: Int): BackupManga { private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
// Entry for this manga // Entry for this manga
val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */) val mangaObject = BackupManga.copyFrom(manga)
// SY --> // SY -->
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
@@ -236,13 +237,24 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
/** /**
* Fetches manga information * Fetches manga information
* *
* @param source source of manga
* @param manga manga that needs updating * @param manga manga that needs updating
* @return Updated manga info. * @return Updated manga info.
*/ */
fun restoreManga(manga: Manga): Manga { suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
return manga.also { return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) {
it.initialized = it.description != null val networkManga = source.getMangaDetails(manga.toMangaInfo())
it.id = insertManga(it) manga.also {
it.copyFrom(networkManga.toSManga())
it.favorite = manga.favorite
it.initialized = true
it.id = insertManga(manga)
}
} else {
manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
} }
} }
@@ -351,26 +363,29 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val trackToUpdate = mutableListOf<Track>() val trackToUpdate = mutableListOf<Track>()
tracks.forEach { track -> tracks.forEach { track ->
var isInDatabase = false val service = trackManager.getService(track.sync_id)
for (dbTrack in dbTracks) { if (service != null && service.isLogged) {
if (track.sync_id == dbTrack.sync_id) { var isInDatabase = false
// The sync is already in the db, only update its fields for (dbTrack in dbTracks) {
if (track.media_id != dbTrack.media_id) { if (track.sync_id == dbTrack.sync_id) {
dbTrack.media_id = track.media_id // The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
} if (!isInDatabase) {
if (!isInDatabase) { // Insert new sync. Let the db assign the id
// Insert new sync. Let the db assign the id track.id = null
track.id = null trackToUpdate.add(track)
trackToUpdate.add(track) }
} }
} }
// Update database // Update database
@@ -379,7 +394,47 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) { /**
* Restore the chapters for manga if chapters already in database
*
* @param manga manga of chapters
* @param chapters list containing chapters that get restored
* @return boolean answering if chapter fetch is not needed
*/
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false
}
chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
chapter.read = dbChapter.read
chapter.last_page_read = dbChapter.last_page_read
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
chapter.last_page_read = dbChapter.last_page_read
}
if (!chapter.bookmark && dbChapter.bookmark) {
chapter.bookmark = dbChapter.bookmark
}
}
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter -> chapters.forEach { chapter ->
@@ -472,9 +527,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) { internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
val mangaId = manga.id ?: return manga.id?.let { mangaId ->
launchIO {
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let { databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
if (it == null) { if (it == null) {
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId) val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
@@ -15,7 +15,8 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.all.MergedSource
import exh.EXHMigrations import exh.EXHMigrations
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import okio.buffer import okio.buffer
@@ -23,7 +24,7 @@ import okio.gzip
import okio.source import okio.source
import java.util.Date import java.util.Date
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) { class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override suspend fun performRestore(uri: Uri): Boolean { override suspend fun performRestore(uri: Uri): Boolean {
// SY --> // SY -->
@@ -56,11 +57,9 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
return false return false
} }
restoreManga(it, backup.backupCategories) restoreManga(it, backup.backupCategories, online)
} }
// TODO: optionally trigger online library + tracker update
return true return true
} }
@@ -82,8 +81,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
} }
// SY <-- // SY <--
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) { private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
val manga = backupManga.getMangaImpl() var manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl() val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories val categories = backupManga.categories
val history = backupManga.history val history = backupManga.history
@@ -91,17 +90,22 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
// SY --> // SY -->
val mergedMangaReferences = backupManga.mergedMangaReferences val mergedMangaReferences = backupManga.mergedMangaReferences
val flatMetadata = backupManga.flatMetadata val flatMetadata = backupManga.flatMetadata
val customManga = backupManga.getCustomMangaInfo()
// SY <-- // SY <--
// SY --> // SY -->
EXHMigrations.migrateBackupEntry(manga) manga = EXHMigrations.migrateBackupEntry(manga)
// SY <-- // SY <--
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try { try {
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) if (source != null || !online) {
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
} else {
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
} }
@@ -113,35 +117,35 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
* Returns a manga restore observable * Returns a manga restore observable
* *
* @param manga manga data from json * @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json * @param chapters chapters data from json
* @param categories categories data from json * @param categories categories data from json
* @param history history data from json * @param history history data from json
* @param tracks tracking data from json * @param tracks tracking data from json
*/ */
private fun restoreMangaData( private suspend fun restoreMangaData(
manga: Manga, manga: Manga,
source: Source?,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
customManga: CustomMangaManager.MangaJson?, online: Boolean
// SY -->
) { ) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction { db.inTransaction {
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) { if (dbManga == null) {
// Manga not in database // Manga not in database
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
} else { } else { // Manga in database
// Manga in database
// Copy information from manga already in database // Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga) backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information // Fetch rest of manga information
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
} }
} }
} }
@@ -153,60 +157,66 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
* @param chapters chapters of manga that needs updating * @param chapters chapters of manga that needs updating
* @param categories categories that need updating * @param categories categories that need updating
*/ */
private fun restoreMangaFetch( private suspend fun restoreMangaFetch(
source: Source?,
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
customManga: CustomMangaManager.MangaJson?, online: Boolean
// SY <--
) { ) {
try { try {
val fetchedManga = backupManager.restoreManga(manga) val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
fetchedManga.id ?: return fetchedManga.id ?: return
backupManager.restoreChaptersForManga(fetchedManga, chapters)
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) if (online && source != null) {
// SY -->
if (source !is MergedSource) {
updateChapters(source, fetchedManga, chapters)
}
// SY <--
} else {
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
}
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}") errors.add(Date() to "${manga.title} - ${e.message}")
} }
} }
private fun restoreMangaNoFetch( private suspend fun restoreMangaNoFetch(
source: Source?,
backupManga: Manga, backupManga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
customManga: CustomMangaManager.MangaJson?, online: Boolean
// SY <--
) { ) {
backupManager.restoreChaptersForManga(backupManga, chapters) if (online && source != null) {
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
} else {
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
updateTracking(backupManga, tracks)
} }
private fun restoreExtraForManga( private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
manga: Manga,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?,
customManga: CustomMangaManager.MangaJson?,
// SY <--
) {
// Restore categories // Restore categories
backupManager.restoreCategoriesForManga(manga, categories, backupCategories) backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
@@ -222,10 +232,6 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
// Restore flat metadata for metadata sources // Restore flat metadata for metadata sources
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) } flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
// Restore Custom Info
customManga?.id = manga.id!!
customManga?.let { customMangaManager.saveMangaInfo(it) }
// SY <-- // SY <--
} }
} }
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@@ -37,15 +36,7 @@ data class BackupManga(
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(), @ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
// SY specific values // SY specific values
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(), @ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null, @ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null
@ProtoNumber(602) var customStatus: Int = 0,
// J2K specific values
@ProtoNumber(800) var customTitle: String? = null,
@ProtoNumber(801) var customArtist: String? = null,
@ProtoNumber(802) var customAuthor: String? = null,
@ProtoNumber(803) var customDescription: String? = null,
@ProtoNumber(803) var customGenre: List<String>? = null
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl().apply { return MangaImpl().apply {
@@ -71,29 +62,6 @@ data class BackupManga(
} }
} }
// SY -->
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
if (customTitle != null ||
customArtist != null ||
customAuthor != null ||
customDescription != null ||
customGenre != null ||
customStatus != 0
) {
return CustomMangaManager.MangaJson(
id = 0L,
title = customTitle,
author = customAuthor,
artist = customArtist,
description = customDescription,
genre = customGenre,
status = customStatus.takeUnless { it == 0 }
)
}
return null
}
// SY <--
fun getTrackingImpl(): List<TrackImpl> { fun getTrackingImpl(): List<TrackImpl> {
return tracking.map { return tracking.map {
it.getTrackingImpl() it.getTrackingImpl()
@@ -101,35 +69,22 @@ data class BackupManga(
} }
companion object { companion object {
fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga { fun copyFrom(manga: Manga): BackupManga {
return BackupManga( return BackupManga(
url = manga.url, url = manga.url,
// SY --> title = manga.title,
title = manga.originalTitle, artist = manga.artist,
artist = manga.originalArtist, author = manga.author,
author = manga.originalAuthor, description = manga.description,
description = manga.originalDescription, genre = manga.getGenres() ?: emptyList(),
genre = manga.getOriginalGenres() ?: emptyList(), status = manga.status,
status = manga.originalStatus,
// SY <--
thumbnailUrl = manga.thumbnail_url, thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite, favorite = manga.favorite,
source = manga.source, source = manga.source,
dateAdded = manga.date_added, dateAdded = manga.date_added,
viewer = manga.viewer, viewer = manga.viewer,
chapterFlags = manga.chapter_flags chapterFlags = manga.chapter_flags
// SY --> )
).also { backupManga ->
customMangaManager?.getManga(manga)?.let {
backupManga.customTitle = it.title
backupManga.customArtist = it.artist
backupManga.customAuthor = it.author
backupManga.customDescription = it.description
backupManga.customGenre = it.getGenres()
backupManga.customStatus = it.status
}
}
// SY <--
} }
} }
} }
@@ -91,7 +91,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
// SY <-- // SY <--
private suspend fun restoreManga(mangaJson: JsonObject) { private suspend fun restoreManga(mangaJson: JsonObject) {
val manga = backupManager.parser.fromJson<MangaImpl>( /* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
mangaJson.get( mangaJson.get(
Backup.MANGA Backup.MANGA
) )
@@ -114,7 +114,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
) )
// EXH --> // EXH -->
EXHMigrations.migrateBackupEntry(manga) manga = EXHMigrations.migrateBackupEntry(manga)
// <-- EXH // <-- EXH
val source = backupManager.sourceManager.get(manga.source) val source = backupManager.sourceManager.get(manga.source)
@@ -59,8 +59,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_DESCRIPTION to obj.originalDescription, COL_DESCRIPTION to obj.originalDescription,
COL_GENRE to obj.originalGenre, COL_GENRE to obj.originalGenre,
COL_TITLE to obj.originalTitle, COL_TITLE to obj.originalTitle,
COL_STATUS to obj.originalStatus,
// SY <-- // SY <--
COL_STATUS to obj.status,
COL_THUMBNAIL_URL to obj.thumbnail_url, COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite, COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update, COL_LAST_UPDATE to obj.last_update,
@@ -40,12 +40,10 @@ open class MangaImpl : Manga {
override var genre: String? override var genre: String?
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
set(value) { ogGenre = value } set(value) { ogGenre = value }
override var status: Int
get() = if (favorite) customMangaManager.getManga(this)?.status?.takeUnless { it == 0 } ?: ogStatus else ogStatus
set(value) { ogStatus = value }
// SY <-- // SY <--
override var status: Int = 0
override var thumbnail_url: String? = null override var thumbnail_url: String? = null
override var favorite: Boolean = false override var favorite: Boolean = false
@@ -73,8 +71,6 @@ open class MangaImpl : Manga {
private set private set
var ogGenre: String? = null var ogGenre: String? = null
private set private set
var ogStatus: Int = 0
private set
// SY <-- // SY <--
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.CategoryTable
@@ -103,11 +102,6 @@ interface MangaQueries : DbProvider {
.`object`(manga) .`object`(manga)
.withPutResolver(MangaMigrationPutResolver()) .withPutResolver(MangaMigrationPutResolver())
.prepare() .prepare()
fun updateMangaThumbnail(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaThumbnailPutResolver())
.prepare()
// SY <-- // SY <--
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
@@ -157,38 +151,12 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where( .where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
)
""".trimIndent()
)
.whereArgs(0) .whereArgs(0)
.build() .build()
) )
.prepare() .prepare()
// SY -->
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
)
""".trimIndent()
)
.whereArgs(0)
.build()
)
.prepare()
// SY <--
fun deleteMangas() = db.delete() fun deleteMangas() = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
@@ -227,16 +195,6 @@ interface MangaQueries : DbProvider {
) )
.prepare() .prepare()
fun getChapterFetchDateManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
)
.prepare()
// SY --> // SY -->
fun getMangaWithMetadata() = db.get() fun getMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
@@ -69,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN( WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0 SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1
) )
""" """
@@ -221,16 +221,6 @@ fun getLatestChapterMangaQuery() =
ORDER by max DESC ORDER by max DESC
""" """
fun getChapterFetchDateMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
/** /**
* Query to get the categories for a manga. * Query to get the categories for a manga.
*/ */
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
@@ -8,7 +9,6 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import exh.util.nullIfZero
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() { class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
@@ -31,20 +31,15 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
MangaTable.COL_GENRE to manga.originalGenre, MangaTable.COL_GENRE to manga.originalGenre,
MangaTable.COL_AUTHOR to manga.originalAuthor, MangaTable.COL_AUTHOR to manga.originalAuthor,
MangaTable.COL_ARTIST to manga.originalArtist, MangaTable.COL_ARTIST to manga.originalArtist,
MangaTable.COL_DESCRIPTION to manga.originalDescription, MangaTable.COL_DESCRIPTION to manga.originalDescription
MangaTable.COL_STATUS to manga.originalStatus
) )
private fun resetToContentValues(manga: Manga) = contentValuesOf( fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
MangaTable.COL_TITLE to manga.title.split(splitter).last(), val splitter = "▒ ▒∩▒"
MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(), put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(), put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(), put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(), put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull() put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
)
companion object {
const val splitter = "▒ ▒∩▒"
} }
} }
@@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
// SY
class MangaThumbnailPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
)
}
@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -24,7 +23,7 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
*/ */
class DownloadManager(private val context: Context) { class DownloadManager(/* SY private */ val context: Context) {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@@ -212,16 +211,16 @@ class DownloadManager(private val context: Context) {
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters) val filteredChapters = getChaptersToDelete(chapters)
launchIO {
removeFromDownloadQueue(filteredChapters)
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source) removeFromDownloadQueue(filteredChapters)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga) val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty chapterDirs.forEach { it.delete() }
chapterDirs.firstOrNull()?.parentFile?.delete() cache.removeChapters(filteredChapters, manga)
} if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
} }
return filteredChapters return filteredChapters
} }
@@ -303,11 +302,9 @@ class DownloadManager(private val context: Context) {
* @param source the source of the manga. * @param source the source of the manga.
*/ */
fun deleteManga(manga: Manga, source: Source) { fun deleteManga(manga: Manga, source: Source) {
launchIO { downloader.queue.remove(manga)
downloader.queue.remove(manga) provider.findMangaDir(manga, source)?.delete()
provider.findMangaDir(manga, source)?.delete() cache.removeManga(manga)
cache.removeManga(manga)
}
} }
/** /**
@@ -27,9 +27,6 @@ internal class DownloadNotifier(private val context: Context) {
private val progressNotificationBuilder by lazy { private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
} }
} }
@@ -87,6 +84,7 @@ internal class DownloadNotifier(private val context: Context) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
setAutoCancel(false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@@ -129,6 +127,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.chapter_paused)) setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
setAutoCancel(false)
setProgress(0, 0, false) setProgress(0, 0, false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
@@ -218,6 +217,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions() clearActions()
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
@@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
return downloadsDir return downloadsDir
.createDirectory(getSourceDirName(source)) .createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga)) .createDirectory(getMangaDirName(manga))
} catch (e: Throwable) { } catch (e: NullPointerException) {
Timber.e(e, "Invalid download directory") Timber.w(e)
throw Exception(context.getString(R.string.invalid_download_dir)) throw Exception(context.getString(R.string.invalid_download_dir))
} }
} }
@@ -8,6 +8,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.util.Scanner
class CustomMangaManager(val context: Context) { class CustomMangaManager(val context: Context) {
@@ -22,7 +23,7 @@ class CustomMangaManager(val context: Context) {
val json = try { val json = try {
Json.decodeFromString<MangaList>( Json.decodeFromString<MangaList>(
editJson.bufferedReader().use { it.readText() } Scanner(editJson).useDelimiter("\\Z").next()
) )
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -31,15 +32,30 @@ class CustomMangaManager(val context: Context) {
val mangasJson = json.mangas ?: return mutableMapOf() val mangasJson = json.mangas ?: return mutableMapOf()
return mangasJson.mapNotNull { mangaJson -> return mangasJson.mapNotNull { mangaJson ->
val id = mangaJson.id ?: return@mapNotNull null val id = mangaJson.id ?: return@mapNotNull null
id to mangaJson.toManga() val manga = MangaImpl().apply {
this.id = id
title = mangaJson.title ?: ""
author = mangaJson.author
artist = mangaJson.artist
description = mangaJson.description
genre = mangaJson.genre?.joinToString(", ")
}
id to manga
}.toMap().toMutableMap() }.toMap().toMutableMap()
} }
fun saveMangaInfo(manga: MangaJson) { fun saveMangaInfo(manga: MangaJson) {
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null && manga.status == null) { if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
customMangaMap.remove(manga.id!!) customMangaMap.remove(manga.id!!)
} else { } else {
customMangaMap[manga.id!!] = manga.toManga() customMangaMap[manga.id!!] = MangaImpl().apply {
id = manga.id
title = manga.title ?: ""
author = manga.author
artist = manga.artist
description = manga.description
genre = manga.genre?.joinToString(", ")
}
} }
saveCustomInfo() saveCustomInfo()
} }
@@ -59,8 +75,7 @@ class CustomMangaManager(val context: Context) {
author, author,
artist, artist,
description, description,
genre?.split(", "), genre?.split(", ")
status
) )
} }
@@ -71,23 +86,24 @@ class CustomMangaManager(val context: Context) {
@Serializable @Serializable
data class MangaJson( data class MangaJson(
var id: Long? = null, val id: Long? = null,
val title: String? = null, val title: String? = null,
val author: String? = null, val author: String? = null,
val artist: String? = null, val artist: String? = null,
val description: String? = null, val description: String? = null,
val genre: List<String>? = null, val genre: List<String>? = null
val status: Int? = null
) { ) {
fun toManga() = MangaImpl().apply { override fun equals(other: Any?): Boolean {
id = this@MangaJson.id if (this === other) return true
title = this@MangaJson.title ?: "" if (javaClass != other?.javaClass) return false
author = this@MangaJson.author other as MangaJson
artist = this@MangaJson.artist if (id != other.id) return false
description = this@MangaJson.description return true
genre = this@MangaJson.genre?.joinToString(", ") }
status = this@MangaJson.status ?: 0
override fun hashCode(): Int {
return id.hashCode()
} }
} }
} }
@@ -111,7 +111,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent) setContentIntent(errorLogIntent)
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_show_errors), context.getString(R.string.action_open_log),
errorLogIntent errorLogIntent
) )
} }
@@ -41,7 +41,7 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadata
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
@@ -88,7 +88,6 @@ class LibraryUpdateService(
private lateinit var notifier: LibraryUpdateNotifier private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope private lateinit var ioScope: CoroutineScope
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
private var updateJob: Job? = null private var updateJob: Job? = null
/** /**
@@ -110,8 +109,6 @@ class LibraryUpdateService(
companion object { companion object {
private var instance: LibraryUpdateService? = null
/** /**
* Key for category to update. * Key for category to update.
*/ */
@@ -150,7 +147,7 @@ class LibraryUpdateService(
* @return true if service newly started, false otherwise * @return true if service newly started, false otherwise
*/ */
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean { fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
return if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply { val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target) putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) } category?.let { putExtra(KEY_CATEGORY, it.id) }
@@ -161,11 +158,10 @@ class LibraryUpdateService(
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
true return true
} else {
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target)
false
} }
return false
} }
/** /**
@@ -202,9 +198,6 @@ class LibraryUpdateService(
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
} }
if (instance == this) {
instance = null
}
super.onDestroy() super.onDestroy()
} }
@@ -228,27 +221,23 @@ class LibraryUpdateService(
val target = intent.getSerializableExtra(KEY_TARGET) as? Target val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY ?: return START_NOT_STICKY
instance = this // Unsubscribe from any previous subscription if needed.
// Unsubscribe from any previous subscription if needed
updateJob?.cancel() updateJob?.cancel()
// Update favorite manga // Update favorite manga. Destroy service when completed or in case of an error.
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val selectedScheme = preferences.libraryUpdatePrioritization().get()
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT) val mangaList = getMangaToUpdate(intent, target)
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA) .sortedWith(rankingScheme[selectedScheme])
addMangaToQueue(categoryId, group, groupExtra, target)
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception) Timber.e(exception)
stopSelf(startId) stopSelf(startId)
} }
updateJob = ioScope.launch(handler) { updateJob = ioScope.launch(handler) {
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList() Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers() Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings() Target.TRACKING -> updateTrackings(mangaList)
// SY --> // SY -->
Target.SYNC_FOLLOWS -> syncFollows() Target.SYNC_FOLLOWS -> syncFollows()
Target.PUSH_FAVORITES -> pushFavorites() Target.PUSH_FAVORITES -> pushFavorites()
@@ -261,40 +250,36 @@ class LibraryUpdateService(
} }
/** /**
* Adds list of manga to be updated. * Returns the list of manga to be updated.
* *
* @param category the ID of the category to update, or -1 if no category specified. * @param intent the update intent.
* @param target the target to update. * @param target the target to update.
* @return a list of manga to update
*/ */
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?, target: Target) { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val libraryManga = db.getLibraryMangas().executeAsBlocking() val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
// SY --> // SY -->
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get() val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
// SY <-- // SY <--
var listToUpdate = if (categoryId != -1) { var listToUpdate = if (categoryId != -1) {
libraryManga.filter { it.category == categoryId } db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
// SY --> // SY -->
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) { } else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
val listToInclude = if (categoriesToUpdate.isNotEmpty()) { if (categoriesToUpdate.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToUpdate } db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
} else { } else {
libraryManga db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
} }
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToExclude }
} else {
emptyList()
}
listToInclude.minus(listToExclude)
} else { } else {
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
when (group) { when (group) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val trackingExtra = groupExtra?.toIntOrNull() ?: -1 val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter { libraryManga.filter {
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
val status: String = run { val status: String = run {
@@ -313,12 +298,12 @@ class LibraryUpdateService(
} }
} }
LibraryGroup.BY_SOURCE -> { LibraryGroup.BY_SOURCE -> {
val sourceExtra = groupExtra.nullIfBlank() val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra } val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
if (source != null) libraryManga.filter { it.source == source.id } else emptyList() if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
} }
LibraryGroup.BY_STATUS -> { LibraryGroup.BY_STATUS -> {
val statusExtra = groupExtra?.toIntOrNull() ?: -1 val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter { libraryManga.filter {
it.status == statusExtra it.status == statusExtra
} }
@@ -329,13 +314,10 @@ class LibraryUpdateService(
// SY <-- // SY <--
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED } listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
} }
val selectedScheme = preferences.libraryUpdatePrioritization().get() return listToUpdate
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
} }
/** /**
@@ -347,7 +329,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
suspend fun updateChapterList() { suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInteger(0)
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
@@ -481,7 +463,7 @@ class LibraryUpdateService(
return syncChaptersWithSource(db, chapters, manga, source) return syncChaptersWithSource(db, chapters, manga, source)
} }
private suspend fun updateCovers() { private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
var progressCount = 0 var progressCount = 0
mangaToUpdate.forEach { manga -> mangaToUpdate.forEach { manga ->
@@ -514,7 +496,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a * Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here. * background thread, so it's safe to do heavy operations or network calls here.
*/ */
private suspend fun updateTrackings() { private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
var progressCount = 0 var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
@@ -557,12 +539,11 @@ class LibraryUpdateService(
private suspend fun syncFollows() { private suspend fun syncFollows() {
val count = AtomicInteger(0) val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
val size: Int val size: Int
mangaDex.fetchAllFollows(true) mangaDex.fetchAllFollows(true)
.filter { (_, metadata) -> .filter { (_, metadata) ->
syncFollowStatusInts.contains(metadata.follow_status) metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
} }
.also { size = it.size } .also { size = it.size }
.forEach { (networkManga, metadata) -> .forEach { (networkManga, metadata) ->
@@ -588,7 +569,7 @@ class LibraryUpdateService(
val id = db.insertManga(dbManga).executeOnIO().insertedId() val id = db.insertManga(dbManga).executeOnIO().insertedId()
if (id != null) { if (id != null) {
metadata.mangaId = id metadata.mangaId = id
db.insertFlatMetadataAsync(metadata.flatten()).await() db.insertFlatMetadata(metadata.flatten()).await()
} }
} }
@@ -74,7 +74,7 @@ class NotificationReceiver : BroadcastReceiver() {
shareFile( shareFile(
context, context,
intent.getParcelableExtra(EXTRA_URI), intent.getParcelableExtra(EXTRA_URI),
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip", if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
ACTION_CANCEL_RESTORE -> cancelRestore( ACTION_CANCEL_RESTORE -> cancelRestore(
@@ -17,21 +17,13 @@ object PreferenceKeys {
const val rotation = "pref_rotation_type_key" const val rotation = "pref_rotation_type_key"
const val enableTransitionsPager = "pref_enable_transitions_pager_key" const val enableTransitions = "pref_enable_transitions_key"
const val enableTransitionsWebtoon = "pref_enable_transitions_webtoon_key"
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed" const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
const val showPageNumber = "pref_show_page_number_key" const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplitPaged = "pref_dual_page_split" const val dualPageSplit = "pref_dual_page_split"
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
const val dualPageInvertPaged = "pref_dual_page_invert"
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
const val showReadingMode = "pref_show_reading_mode" const val showReadingMode = "pref_show_reading_mode"
@@ -81,10 +73,6 @@ object PreferenceKeys {
const val navigationModeWebtoon = "reader_navigation_mode_webtoon" const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
const val webtoonSidePadding = "webtoon_side_padding" const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key" const val portraitColumns = "pref_library_columns_portrait_key"
@@ -126,7 +114,6 @@ object PreferenceKeys {
const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateRestriction = "library_update_restriction"
const val libraryUpdateCategories = "library_update_categories" const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization" const val libraryUpdatePrioritization = "library_update_prioritization"
@@ -171,7 +158,6 @@ object PreferenceKeys {
const val downloadNew = "download_new" const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories" const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val libraryDisplayMode = "pref_display_mode_library" const val libraryDisplayMode = "pref_display_mode_library"
@@ -197,7 +183,7 @@ object PreferenceKeys {
const val searchPinnedSourcesOnly = "search_pinned_sources_only" const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val dohProvider = "doh_provider" const val enableDoh = "enable_doh"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read" const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
@@ -213,8 +199,6 @@ object PreferenceKeys {
const val incognitoMode = "incognito_mode" const val incognitoMode = "incognito_mode"
const val createLegacyBackup = "create_legacy_backup"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
@@ -329,8 +313,6 @@ object PreferenceKeys {
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key" const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
const val preferredMangaDexId = "preferred_mangaDex_id" const val preferredMangaDexId = "preferred_mangaDex_id"
const val dataSaver = "data_saver" const val dataSaver = "data_saver"
@@ -357,15 +339,11 @@ object PreferenceKeys {
const val sortTagsForLibrary = "sort_tags_for_library" const val sortTagsForLibrary = "sort_tags_for_library"
const val createLegacyBackup = "create_legacy_backup"
const val dontDeleteFromCategories = "dont_delete_from_categories" const val dontDeleteFromCategories = "dont_delete_from_categories"
const val extensionRepos = "extension_repos" const val extensionRepos = "extension_repos"
const val cropBordersContinuousVertical = "crop_borders_continues_vertical" const val cropBordersContinuesVertical = "crop_borders_continues_vertical"
const val landscapeVerticalSeekbar = "pref_show_vert_seekbar_landscape"
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
} }
@@ -5,8 +5,6 @@ package eu.kanade.tachiyomi.data.preference
*/ */
object PreferenceValues { object PreferenceValues {
/* ktlint-disable experimental:enum-entry-name-case */
// Keys are lowercase to match legacy string values // Keys are lowercase to match legacy string values
enum class ThemeMode { enum class ThemeMode {
light, light,
@@ -27,11 +25,8 @@ object PreferenceValues {
amoled, amoled,
red, red,
midnightdusk, midnightdusk,
hotpink,
} }
/* ktlint-enable experimental:enum-entry-name-case */
enum class DisplayMode { enum class DisplayMode {
COMPACT_GRID, COMPACT_GRID,
COMFORTABLE_GRID, COMFORTABLE_GRID,
@@ -22,7 +22,7 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> { fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
block(get()) block(get())
return asFlow() return asFlow()
.onEach { block(it) } .onEach { block(it) }
@@ -36,10 +36,6 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item) set(get() - item)
} }
fun Preference<Boolean>.toggle() {
set(!get())
}
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@@ -87,21 +83,13 @@ class PreferencesHelper(val context: Context) {
fun rotation() = flowPrefs.getInt(Keys.rotation, 1) fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true) fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500) fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true) fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false) fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true) fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
@@ -155,10 +143,6 @@ class PreferencesHelper(val context: Context) {
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0) fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0) fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0) fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
@@ -220,7 +204,6 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
@@ -271,7 +254,6 @@ class PreferencesHelper(val context: Context) {
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false) fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet()) fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun lang() = prefs.getString(Keys.lang, "") fun lang() = prefs.getString(Keys.lang, "")
@@ -285,7 +267,7 @@ class PreferencesHelper(val context: Context) {
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1) fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "") fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
@@ -443,8 +425,6 @@ class PreferencesHelper(val context: Context) {
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true) fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2) fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false) fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
@@ -475,11 +455,5 @@ class PreferencesHelper(val context: Context) {
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet()) fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
fun cropBordersContinuousVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuousVertical, false) fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false)
fun forceHorizontalSeekbar() = flowPrefs.getBoolean(Keys.forceHorizontalSeekbar, false)
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
} }
@@ -35,8 +35,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
private val api by lazy { AnilistApi(client, interceptor) } private val api by lazy { AnilistApi(client, interceptor) }
override val supportsReadingDates: Boolean = true
private val scorePreference = preferences.anilistScoreType() private val scorePreference = preferences.anilistScoreType()
init { init {
@@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.afollestad.date.dayOfMonth
import com.afollestad.date.month
import com.afollestad.date.year
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@@ -12,7 +9,6 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
@@ -34,7 +30,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = """ val query =
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id | id
@@ -68,15 +65,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = """ val query =
|mutation UpdateManga( """
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) {
|id |id
|status |status
|progress |progress
@@ -90,8 +82,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("progress", track.last_chapter_read) put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus()) put("status", track.toAnilistStatus())
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date))
} }
} }
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
@@ -102,7 +92,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val query = """ val query =
"""
|query Search(${'$'}query: String) { |query Search(${'$'}query: String) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@@ -152,7 +143,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun findLibManga(track: Track, userid: Int): Track? { suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext { return withIOContext {
val query = """ val query =
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page { |Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@@ -160,16 +152,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status |status
|scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
|progress |progress
|startedAt {
|year
|month
|day
|}
|completedAt {
|year
|month
|day
|}
|media { |media {
|id |id
|title { |title {
@@ -227,7 +209,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun getCurrentUser(): Pair<Int, String> { suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext { return withIOContext {
val query = """ val query =
"""
|query User { |query User {
|Viewer { |Viewer {
|id |id
@@ -260,6 +243,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
private fun jsonToALManga(struct: JsonObject): ALManga { private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
(
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
?: 0
) - 1,
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga( return ALManga(
struct["id"]!!.jsonPrimitive.int, struct["id"]!!.jsonPrimitive.int,
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content, struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
@@ -267,7 +265,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["description"]!!.jsonPrimitive.contentOrNull, struct["description"]!!.jsonPrimitive.contentOrNull,
struct["type"]!!.jsonPrimitive.content, struct["type"]!!.jsonPrimitive.content,
struct["status"]!!.jsonPrimitive.contentOrNull ?: "", struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
parseDate(struct, "startDate"), date,
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0 struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
) )
} }
@@ -278,44 +276,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["status"]!!.jsonPrimitive.content, struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int, struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int, struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject) jsonToALManga(struct["media"]!!.jsonObject)
) )
} }
private fun parseDate(struct: JsonObject, dateKey: String): Long {
return try {
val date = Calendar.getInstance()
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
)
date.timeInMillis
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
return buildJsonObject {
put("year", JsonNull)
put("month", JsonNull)
put("day", JsonNull)
}
}
val calendar = Calendar.getInstance()
calendar.timeInMillis = dateValue
return buildJsonObject {
put("year", calendar.year)
put("month", calendar.month + 1)
put("day", calendar.dayOfMonth)
}
}
companion object { companion object {
private const val clientId = "385" private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/" private const val apiUrl = "https://graphql.anilist.co/"
@@ -44,8 +44,6 @@ data class ALUserManga(
val list_status: String, val list_status: String,
val score_raw: Int, val score_raw: Int,
val chapters_read: Int, val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga val manga: ALManga
) { ) {
@@ -53,8 +51,6 @@ data class ALUserManga(
media_id = manga.media_id media_id = manga.media_id
status = toTrackStatus() status = toTrackStatus()
score = score_raw.toFloat() score = score_raw.toFloat()
started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read last_chapter_read = chapters_read
library_id = this@ALUserManga.library_id library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters total_chapters = manga.total_chapters
@@ -45,10 +45,8 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return if (remoteTrack != null && statusTrack != null) { return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
track.status = statusTrack.status track.status = remoteTrack.status
track.score = statusTrack.score track.last_chapter_read = remoteTrack.last_chapter_read
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track) refresh(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
@@ -68,6 +66,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
track.copyPersonalFrom(remoteStatusTrack!!) track.copyPersonalFrom(remoteStatusTrack!!)
api.findLibManga(track)?.let { remoteTrack -> api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
} }
return track return track
} }
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@@ -47,7 +46,6 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return withIOContext { return withIOContext {
// read status update // read status update
val sbody = FormBody.Builder() val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody)) authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
@@ -93,24 +91,12 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
private fun jsonToSearch(obj: JsonObject): TrackSearch { private fun jsonToSearch(obj: JsonObject): TrackSearch {
val coverUrl = if (obj["images"] is JsonObject) {
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
} else {
// Sometimes JsonNull
""
}
val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int
} else {
0
}
return TrackSearch.create(TrackManager.BANGUMI).apply { return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"]!!.jsonPrimitive.int media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name_cn"]!!.jsonPrimitive.content title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = coverUrl cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
summary = obj["name"]!!.jsonPrimitive.content summary = obj["name"]!!.jsonPrimitive.content
tracking_url = obj["url"]!!.jsonPrimitive.content tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
} }
} }
@@ -133,21 +119,14 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.build() .build()
// TODO: get user readed chapter here // TODO: get user readed chapter here
var response = authClient.newCall(requestUserRead).await() authClient.newCall(requestUserRead)
var responseBody = response.body?.string().orEmpty() .await()
if (responseBody.isEmpty()) { .parseAs<Collection>()
throw Exception("Null Response") .let {
}
if (responseBody.contains("\"code\":400")) {
null
} else {
json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!! track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!! track.last_chapter_read = it.ep_status!!
track.score = it.rating!!
track track
} }
}
} }
} }
@@ -8,7 +8,7 @@ data class Collection(
val comment: String? = "", val comment: String? = "",
val ep_status: Int? = 0, val ep_status: Int? = 0,
val lasttouch: Int? = 0, val lasttouch: Int? = 0,
val rating: Float? = 0f, val rating: Int? = 0,
val status: Status? = Status(), val status: Status? = Status(),
val tag: List<String?>? = listOf(), val tag: List<String?>? = listOf(),
val user: User? = User(), val user: User? = User(),
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@@ -47,84 +46,80 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track = update(track) override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
return withIOContext { val mdex = mdex ?: throw MangaDexNotFoundException()
val mdex = mdex ?: throw MangaDexNotFoundException()
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url) val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
val followStatus = FollowStatus.fromInt(track.status) val followStatus = FollowStatus.fromInt(track.status)
// this updates the follow status in the metadata // this updates the follow status in the metadata
// allow follow status to update // allow follow status to update
if (remoteTrack.status != followStatus.int) { if (remoteTrack.status != followStatus.int) {
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus) mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int remoteTrack.status = followStatus.int
} // db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
if (track.score.toInt() > 0) {
mdex.updateRating(track)
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
}
mdex.updateReadingProgress(track)
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
}
track
} }
if (track.score.toInt() > 0) {
mdex.updateRating(track)
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
}
mdex.updateReadingProgress(track)
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
}
return track
} }
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
override suspend fun bind(track: Track): Track = update(refresh(track).also { if (it.status == FollowStatus.UNFOLLOWED.int) it.status = FollowStatus.READING.int }) override suspend fun bind(track: Track): Track = update(refresh(track))
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
return withIOContext { val mdex = mdex ?: throw MangaDexNotFoundException()
val mdex = mdex ?: throw MangaDexNotFoundException() val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) track.copyPersonalFrom(remoteTrack)
track.copyPersonalFrom(remoteTrack) if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
}
track
} }
return track
} }
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track { fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
return Track.create(TrackManager.MDLIST).apply { val track = Track.create(TrackManager.MDLIST)
manga_id = dbManga.id!! track.manga_id = dbManga.id!!
status = FollowStatus.UNFOLLOWED.int track.status = FollowStatus.UNFOLLOWED.int
tracking_url = MdUtil.baseUrl + mdManga.url track.tracking_url = MdUtil.baseUrl + mdManga.url
title = mdManga.title track.title = mdManga.title
} return track
} }
override suspend fun search(query: String): List<TrackSearch> { override suspend fun search(query: String): List<TrackSearch> {
return withIOContext { val mdex = mdex ?: throw MangaDexNotFoundException()
val mdex = mdex ?: throw MangaDexNotFoundException() return mdex.fetchSearchManga(0, query, mdex.getFilterList())
mdex.fetchSearchManga(0, query, mdex.getFilterList()) .flatMap { page ->
.flatMap { page -> runAsObservable({
runAsObservable({ page.mangas.map {
page.mangas.map { toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo())) }
} })
}) }
} .awaitSingle()
.awaitSingle()
}
} }
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply { private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
@@ -136,8 +131,5 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun login(username: String, password: String): Unit = throw Exception("not used") override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
override val isLogged: Boolean
get() = false
class MangaDexNotFoundException : Exception("Mangadex not enabled") class MangaDexNotFoundException : Exception("Mangadex not enabled")
} }
@@ -32,8 +32,8 @@ class GithubUpdateChecker {
.parseAs<GithubRelease>() .parseAs<GithubRelease>()
.let { .let {
// Check if latest version is different from current version // Check if latest version is different from current version
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) { if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
GithubUpdateResult.NewUpdate(it) GithubUpdateResult.NewUpdate(it)
} else { } else {
GithubUpdateResult.NoNewUpdate() GithubUpdateResult.NoNewUpdate()
} }
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogD
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
@@ -156,15 +156,15 @@ class ExtensionManager(
// EXH --> // EXH -->
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> { private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
val blacklistEnabled = preferences.enableSourceBlacklist().get() val blacklistEnabled = preferences.enableSourceBlacklist().get()
return filterNot { extension -> return filter {
extension.isBlacklisted(blacklistEnabled) if (it.isBlacklisted(blacklistEnabled)) {
.also { XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: %s, pkgName: %s)!", it.name, it.pkgName)
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName) false
} } else true
} }
} }
private fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean { fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
} }
// EXH <-- // EXH <--
@@ -333,7 +333,7 @@ class ExtensionManager(
private fun registerNewExtension(extension: Extension.Installed) { private fun registerNewExtension(extension: Extension.Installed) {
// SY --> // SY -->
if (extension.isBlacklisted()) { if (extension.isBlacklisted()) {
xLogD("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName) XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
return return
} }
// SY <-- // SY <--
@@ -351,7 +351,7 @@ class ExtensionManager(
private fun registerUpdatedExtension(extension: Extension.Installed) { private fun registerUpdatedExtension(extension: Extension.Installed) {
// SY --> // SY -->
if (extension.isBlacklisted()) { if (extension.isBlacklisted()) {
xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName) XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
return return
} }
// SY <-- // SY <--
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
.let { parseResponse(it) } .let { parseResponse(it) }
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath -> } /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
val url = "$BASE_URL$repoPath/repo/" val url = "$BASE_URL$repoPath/repo/"
networkService.client networkService.client
.newCall(GET("${url}index.min.json")) .newCall(GET("${url}index.min.json"))
.await() .await()
.parseAs<JsonArray>() .parseAs<JsonArray>()
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.w(e, "Extension load error: $extName ($it)") Timber.e(e, "Extension load error: $extName.")
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
@@ -171,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
companion object { companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance") private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
} }
} }
@@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
*/
const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
.build()
)
fun OkHttpClient.Builder.dohGoogle() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("8.8.4.4"),
InetAddress.getByName("8.8.8.8")
)
.build()
)
@@ -4,10 +4,13 @@ import android.content.Context
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Cache import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) { /* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
@@ -35,9 +38,25 @@ import java.util.concurrent.TimeUnit
builder.addInterceptor(httpLoggingInterceptor) builder.addInterceptor(httpLoggingInterceptor)
} }
when (preferences.dohProvider()) { if (preferences.enableDoh()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() builder.dns(
PREF_DOH_GOOGLE -> builder.dohGoogle() DnsOverHttps.Builder().client(builder.build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
)
.build()
)
} }
builder.build() builder.build()
@@ -33,7 +33,7 @@ import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(private val context: Context) : CatalogueSource {
companion object { companion object {
const val ID = 0L const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/" const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
private const val COVER_NAME = "cover.jpg" private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub") private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
@@ -64,12 +64,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
val c = context.getString(R.string.app_name) + File.separator + "local" val c = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) } return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
} }
// SY -->
val json = Json {
prettyPrint = true
}
// SY <--
} }
override val id = ID override val id = ID
@@ -157,16 +151,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
// SY --> // SY -->
fun updateMangaInfo(manga: SManga) { fun updateMangaInfo(manga: SManga) {
val directory = getBaseDirectories(context).map { File(it, manga.url) }.find { val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find {
it.exists() it.exists()
} ?: return } ?: return
val json = Json {
prettyPrint = true
}
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
val file = File(directory, existingFileName ?: "info.json") val file = File(directory, existingFileName ?: "info.json")
file.writeText(json.encodeToString(manga.toJson())) file.writeText(json.encodeToString(manga.toJson()))
} }
private fun SManga.toJson(): MangaJson { private fun SManga.toJson(): MangaJson {
return MangaJson(title, author, artist, description, genre?.split(", "), status) return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
} }
@Serializable @Serializable
@@ -175,8 +172,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
val author: String?, val author: String?,
val artist: String?, val artist: String?,
val description: String?, val description: String?,
val genre: List<String>?, val genre: Array<String>?
val status: Int
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -9,14 +10,15 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.Hitomi import eu.kanade.tachiyomi.source.online.all.Hitomi
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden import eu.kanade.tachiyomi.source.online.all.PervEden
import eu.kanade.tachiyomi.source.online.english.EightMuses import eu.kanade.tachiyomi.source.online.english.EightMuses
import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.HBrowse
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import exh.log.xLogD
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
@@ -24,6 +26,7 @@ import exh.source.EIGHTMUSES_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
import exh.source.HBROWSE_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID
import exh.source.HENTAI_CAFE_SOURCE_ID
import exh.source.PERV_EDEN_EN_SOURCE_ID import exh.source.PERV_EDEN_EN_SOURCE_ID
import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.PERV_EDEN_IT_SOURCE_ID
import exh.source.PURURIN_SOURCE_ID import exh.source.PURURIN_SOURCE_ID
@@ -112,7 +115,7 @@ open class SourceManager(private val context: Context) {
} else DELEGATED_SOURCES[sourceQName] } else DELEGATED_SOURCES[sourceQName]
} else null } else null
val newSource = if (source is HttpSource && delegate != null) { val newSource = if (source is HttpSource && delegate != null) {
xLogD("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName) XLog.tag("SourceManager").d("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
val enhancedSource = EnhancedHttpSource( val enhancedSource = EnhancedHttpSource(
source, source,
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context) delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
@@ -129,7 +132,7 @@ open class SourceManager(private val context: Context) {
} else source } else source
if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) { if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) {
xLogD("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang) XLog.tag("SourceManager").d("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang)
return return
} }
// EXH <-- // EXH <--
@@ -152,12 +155,13 @@ open class SourceManager(private val context: Context) {
// SY --> // SY -->
private fun createEHSources(): List<Source> { private fun createEHSources(): List<Source> {
val sources = listOf<HttpSource>( val exSrcs = mutableListOf<HttpSource>(
EHentai(EH_SOURCE_ID, false, context) EHentai(EH_SOURCE_ID, false, context)
) )
return if (prefs.enableExhentai().get()) { if (prefs.enableExhentai().get()) {
sources + EHentai(EXH_SOURCE_ID, true, context) exSrcs += EHentai(EXH_SOURCE_ID, true, context)
} else sources }
return exSrcs
} }
// SY <-- // SY <--
@@ -191,6 +195,12 @@ open class SourceManager(private val context: Context) {
companion object { companion object {
private const val fillInSourceId = Long.MAX_VALUE private const val fillInSourceId = Long.MAX_VALUE
val DELEGATED_SOURCES = listOf( val DELEGATED_SOURCES = listOf(
DelegatedSource(
"Hentai Cafe",
HENTAI_CAFE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
HentaiCafe::class
),
DelegatedSource( DelegatedSource(
"Pururin", "Pururin",
PURURIN_SOURCE_ID, PURURIN_SOURCE_ID,
@@ -203,13 +213,13 @@ open class SourceManager(private val context: Context) {
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class Tsumino::class
), ),
/*DelegatedSource( DelegatedSource(
"MangaDex", "MangaDex",
fillInSourceId, fillInSourceId,
"eu.kanade.tachiyomi.extension.all.mangadex", "eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class, MangaDex::class,
true true
),*/ ),
DelegatedSource( DelegatedSource(
"HBrowse", "HBrowse",
HBROWSE_SOURCE_ID, HBROWSE_SOURCE_ID,
@@ -219,7 +229,7 @@ open class SourceManager(private val context: Context) {
DelegatedSource( DelegatedSource(
"8Muses", "8Muses",
EIGHTMUSES_SOURCE_ID, EIGHTMUSES_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.eightmuses.EightMuses", "eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
EightMuses::class EightMuses::class
), ),
DelegatedSource( DelegatedSource(
@@ -266,7 +276,7 @@ open class SourceManager(private val context: Context) {
get() = internalMap.size get() = internalMap.size
override fun containsKey(key: K): Boolean = internalMap.containsKey(key) override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
override fun containsValue(value: V): Boolean = internalMap.containsValue(value) override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
override fun get(key: K): V? = internalMap[key] override fun get(key: K): V? = get(key)
override fun isEmpty(): Boolean = internalMap.isEmpty() override fun isEmpty(): Boolean = internalMap.isEmpty()
override val entries: MutableSet<MutableMap.MutableEntry<K, V>> override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = internalMap.entries get() = internalMap.entries
@@ -2,26 +2,8 @@ package eu.kanade.tachiyomi.source.model
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
/* SY --> */ open /* SY <-- */ class MangasPage(open val mangas: List<SManga>, open val hasNextPage: Boolean) { /* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
// SY -->
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MangasPage) return false
if (mangas != other.mangas) return false
if (hasNextPage != other.hasNextPage) return false
return true
}
override fun hashCode(): Int {
var result = mangas.hashCode()
result = 31 * result + hasNextPage.hashCode()
return result
}
// SY <--
}
// SY --> // SY -->
data class MetadataMangasPage(override val mangas: List<SManga>, override val hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage) class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
// SY <-- // SY <--
@@ -35,8 +35,6 @@ interface SManga : Serializable {
get() = (this as? MangaImpl)?.ogDesc ?: description get() = (this as? MangaImpl)?.ogDesc ?: description
val originalGenre: String? val originalGenre: String?
get() = (this as? MangaImpl)?.ogGenre ?: genre get() = (this as? MangaImpl)?.ogGenre ?: genre
val originalStatus: Int
get() = (this as? MangaImpl)?.ogStatus ?: status
// SY <-- // SY <--
fun copyFrom(other: SManga) { fun copyFrom(other: SManga) {
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.metadata.metadata.base.insertFlatMetadataCompletable import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.util.executeOnIO import exh.util.executeOnIO
import rx.Completable import rx.Completable
import rx.Single import rx.Single
@@ -72,7 +72,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
}.flatMapCompletable { }.flatMapCompletable {
if (mangaId != null) { if (mangaId != null) {
it.mangaId = mangaId it.mangaId = mangaId
db.insertFlatMetadataCompletable(it.flatten()) db.insertFlatMetadata(it.flatten())
} else Completable.complete() } else Completable.complete()
} }
} }
@@ -87,7 +87,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
parseInfoIntoMetadata(metadata, input) parseInfoIntoMetadata(metadata, input)
if (mangaId != null) { if (mangaId != null) {
metadata.mangaId = mangaId metadata.mangaId = mangaId
db.insertFlatMetadata(metadata.flatten()) db.insertFlatMetadataAsync(metadata.flatten()).await()
} }
return metadata.createMangaInfo(manga) return metadata.createMangaInfo(manga)
@@ -119,7 +119,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
val newMetaSingle = Single.just(newMeta) val newMetaSingle = Single.just(newMeta)
if (mangaId != null) { if (mangaId != null) {
newMeta.mangaId = mangaId newMeta.mangaId = mangaId
db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle) db.insertFlatMetadata(newMeta.flatten()).andThen(newMetaSingle)
} else newMetaSingle } else newMetaSingle
} }
} else Single.just(existingMeta) } else Single.just(existingMeta)
@@ -146,7 +146,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
parseInfoIntoMetadata(newMeta, input) parseInfoIntoMetadata(newMeta, input)
if (mangaId != null) { if (mangaId != null) {
newMeta.mangaId = mangaId newMeta.mangaId = mangaId
db.insertFlatMetadata(newMeta.flatten()).let { newMeta } db.insertFlatMetadataAsync(newMeta.flatten()).await().let { newMeta }
} else newMeta } else newMeta
} }
} }
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -30,7 +31,6 @@ import exh.eh.EHTags
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.eh.EHentaiUpdateWorkerConstants import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry import exh.eh.GalleryEntry
import exh.log.xLogD
import exh.metadata.MetadataUtil import exh.metadata.MetadataUtil
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
@@ -40,7 +40,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.ui.login.EhLoginActivity import exh.ui.login.LoginController
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
@@ -229,7 +229,7 @@ class EHentai(
} else { } else {
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean() parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean()
} }
parsedMangas to hasNextPage Pair(parsedMangas, hasNextPage)
} }
private fun getGenre(element: Element? = null, genreString: String? = null): String? { private fun getGenre(element: Element? = null, genreString: String? = null): String? {
@@ -325,7 +325,7 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(parentLink) url = EHentaiSearchMetadata.normalizeUrl(parentLink)
} else break } else break
} else { } else {
this@EHentai.xLogD("Parent cache hit: %s!", gid) XLog.tag("EHentai").d("Parent cache hit: %s!", gid)
url = EHentaiSearchMetadata.idAndTokenToUrl( url = EHentaiSearchMetadata.idAndTokenToUrl(
cachedParent.gId, cachedParent.gId,
cachedParent.gToken cachedParent.gToken
@@ -613,7 +613,7 @@ class EHentai(
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
) { ) {
aged = true aged = true
this@EHentai.xLogD("aged %s - too old", title) XLog.tag("EHentai").d("aged %s - too old", title)
} }
// Parse ratings // Parse ratings
@@ -713,7 +713,7 @@ class EHentai(
page++ page++
} while (parsed.second) } while (parsed.second)
return Pair(result.toList(), favNames.orEmpty()) return Pair(result.toList(), favNames!!)
} }
fun spPref() = if (exh) { fun spPref() = if (exh) {
@@ -725,9 +725,9 @@ class EHentai(
private fun rawCookies(sp: Int): Map<String, String> { private fun rawCookies(sp: Int): Map<String, String> {
val cookies: MutableMap<String, String> = mutableMapOf() val cookies: MutableMap<String, String> = mutableMapOf()
if (preferences.enableExhentai().get()) { if (preferences.enableExhentai().get()) {
cookies[EhLoginActivity.MEMBER_ID_COOKIE] = preferences.memberIdVal().get() cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
cookies[EhLoginActivity.PASS_HASH_COOKIE] = preferences.passHashVal().get() cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
cookies[EhLoginActivity.IGNEOUS_COOKIE] = preferences.igneousVal().get() cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
cookies["sp"] = sp.toString() cookies["sp"] = sp.toString()
val sessionKey = preferences.exhSettingsKey().get() val sessionKey = preferences.exhSettingsKey().get()
@@ -879,20 +879,13 @@ class EHentai(
stringBuilder.append(" ") stringBuilder.append(" ")
} }
return stringBuilder.toString().trim().also { xLogD(it) } XLog.tag("EHentai").d(stringBuilder.toString())
return stringBuilder.toString().trim()
} }
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean) data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean)
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : Filter.AutoComplete(name = "Tags", hint = "Search tags here (limit of 8)", values = tags, skipAutoFillTags = skipAutoFillTags, excludePrefix = excludePrefix, state = emptyList())
Filter.AutoComplete(
name = "Tags",
hint = "Search tags here (limit of 8)",
values = tags,
skipAutoFillTags = skipAutoFillTags,
excludePrefix = excludePrefix,
state = emptyList()
)
class MinPagesOption : PageOption("Minimum Pages", "f_spf") class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt") class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import androidx.core.text.HtmlCompat
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -47,18 +48,12 @@ import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.EOFException
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@@ -78,10 +73,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
RandomMangaSource { RandomMangaSource {
override val lang: String = delegate.lang override val lang: String = delegate.lang
override val headers: Headers = super.headers.newBuilder().apply { override val headers: Headers
add("X-Requested-With", "XMLHttpRequest") get() = super.headers.newBuilder().apply {
add("Referer", MdUtil.baseUrl) add("X-Requested-With", "XMLHttpRequest")
}.build() add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy { private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang MdLang.values().find { it.lang == lang }?.dexLang ?: lang
@@ -202,26 +198,26 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password) add("login_password", password)
add("no_js", "1") add("no_js", "1")
add("remember_me", "1") add("remember_me", "1")
add("two_factor", twoFactorCode)
} }
runCatching { twoFactorCode.let {
client.newCall( formBody.add("two_factor", it)
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await().closeQuietly()
} }
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await() val response = client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await()
withIOContext { response.body?.string() }.let { jsonData -> withIOContext { response.body?.string() }.let { result ->
if (jsonData != null) { if (result != null && result.isEmpty()) {
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200 true
} else { } else {
throw Exception("Json data was null") val error = result?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() }
throw Exception(error)
} }
} }
} }
@@ -237,17 +233,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
if (token.isNullOrEmpty()) { if (token.isNullOrEmpty()) {
return@withIOContext true return@withIOContext true
} }
try { val result = client.newCall(
val result = client.newCall( POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build() ).await()
).await() val resultStr = withIOContext { result.body?.string() }
val resultStr = withIOContext { result.body?.string() } if (resultStr?.contains("success", true) == true) {
if (resultStr?.contains("success", true) == true) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
} catch (e: EOFException) {
network.cookieManager.remove(httpUrl) network.cookieManager.remove(httpUrl)
trackManager.mdList.logout() trackManager.mdList.logout()
return@withIOContext true return@withIOContext true
@@ -279,7 +269,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> { suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail()) return MangaHandler(client, headers, lang).getTrackingInfo(track, useLowQualityThumbnail())
} }
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
@@ -291,7 +281,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override suspend fun fetchRandomMangaUrl(): String { override suspend fun fetchRandomMangaUrl(): String {
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() } return MangaHandler(client, headers, mdLang).fetchRandomMangaId()
} }
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> { fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.executeOnIO import exh.util.executeOnIO
@@ -181,11 +181,11 @@ class MergedSource : HttpSource() {
} }
manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga()) manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga())
try { try {
manga.id = db.insertManga(manga).executeAsBlocking().insertedId() manga.id = db.insertManga(manga).executeOnIO().insertedId()
mangaId = manga.id mangaId = manga.id
db.insertNewMergedMangaId(this).executeAsBlocking() db.insertNewMergedMangaId(this).executeOnIO()
} catch (e: Exception) { } catch (e: Exception) {
xLogW("Error inserting merged manga id", e) XLog.tag("MergedSource").enableStackTrace(e.stackTrace.contentToString(), 5)
} }
} }
return LoadedMangaSource(source, manga, this) return LoadedMangaSource(source, manga, this)
@@ -0,0 +1,143 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.metadata.metadata.HentaiCafeSearchMetadata
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document
import rx.Observable
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
class HentaiCafe(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
MetadataSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang = "en"
/**
* The class of the metadata used by this source
*/
override val metaClass = HentaiCafeSearchMetadata::class
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup())
}
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: HentaiCafeSearchMetadata, input: Document) {
with(metadata) {
url = input.location()
title = input.select("h3").text()
val contentElement = input.select(".entry-content").first()
thumbnailUrl = contentElement.child(0).child(0).attr("src")
fun filterableTagsOfType(type: String) = contentElement.select("a")
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
.map { it.text() }
tags.clear()
tags += filterableTagsOfType("tag").map {
RaisedTag(null, it, TAG_TYPE_DEFAULT)
}
val artists = filterableTagsOfType("artist")
artist = artists.joinToString()
tags += artists.map {
RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
}
readerId = input.select("[title=Read]").attr("href").toHttpUrlOrNull()!!.pathSegments[2]
}
}
override fun fetchChapterList(manga: SManga) = runAsObservable({
fetchOrLoadMetadata(manga.id) {
val response = client.newCall(mangaDetailsRequest(manga)).await()
response.asJsoup()
}
}).map {
listOf(
SChapter.create().apply {
url = "/manga/read/${it.readerId}/en/0/1/"
name = "Chapter"
chapter_number = 0.0f
}
)
}
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
val metadata = fetchOrLoadMetadata(manga.id()) {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
response.asJsoup()
}
return listOf(
ChapterInfo(
key = "/manga/read/${metadata.readerId}/en/0/1/",
name = "Chapter",
number = 0F
)
)
}
override val matchingHosts = listOf(
"hentai.cafe"
)
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.takeUnless { it.equals("manga", true) } ?: return null
return if (lcFirstPathSegment.equals("hc.fyi", true)) {
"/$lcFirstPathSegment/${uri.pathSegments[1]}"
} else null
}
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
return HentaiCafeDescriptionAdapter(controller)
}
}
@@ -13,7 +13,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val isDarkMode: Boolean by lazy { private val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get() val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) || (themeMode == Values.ThemeMode.dark) ||
( (
@@ -50,7 +50,6 @@ abstract class BaseThemedActivity : AppCompatActivity() {
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
else -> R.style.Theme_Tachiyomi_Dark else -> R.style.Theme_Tachiyomi_Dark
} }
} }
@@ -7,12 +7,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
/** /**
* An [AnimatorChangeHandler] that will remove the from view and fade in the to view * An [AnimatorChangeHandler] that will cross fade two views
*/ */
class OneWayFadeChangeHandler : FadeChangeHandler { class OneWayFadeChangeHandler : AnimatorChangeHandler {
constructor() constructor()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush) constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration) constructor(duration: Long) : super(duration)
@@ -34,6 +33,10 @@ class OneWayFadeChangeHandler : FadeChangeHandler {
return animator return animator
} }
override fun resetFromView(from: View) {
from.alpha = 1f
}
override fun copy(): ControllerChangeHandler { override fun copy(): ControllerChangeHandler {
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush()) return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
} }
@@ -121,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected * [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand] * This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
*/ */
open fun invalidateMenuOnExpand(): Boolean { fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) { return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
false false
@@ -1,196 +0,0 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* Implementation of the NucleusController that has a built-in ViewSearch
*/
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
/**
* Used to bypass the initial searchView being set to empty string after an onResume
*/
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
/**
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
*/
protected var nonSubmittedQuery: String = ""
/**
* To be called by classes that extend this subclass in onCreateOptionsMenu
*/
protected fun createOptionsMenu(
menu: Menu,
inflater: MenuInflater,
menuId: Int,
searchItemId: Int,
@StringRes queryHint: Int? = null,
restoreCurrentQuery: Boolean = true
) {
// Inflate menu
inflater.inflate(menuId, menu)
// Initialize search option.
val searchItem = menu.findItem(searchItemId)
val searchView = searchItem.actionView as SearchView
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
searchView.maxWidth = Int.MAX_VALUE
searchView.queryTextEvents()
.onEach {
val newText = it.queryText.toString()
if (newText.isNotBlank() or acceptEmptyQuery()) {
if (it is QueryTextEvent.QuerySubmitted) {
// Abstract function for implementation
// Run it first in case the old query data is needed (like BrowseSourceController)
onSearchViewQueryTextSubmit(newText)
presenter.query = newText
nonSubmittedQuery = ""
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
nonSubmittedQuery = newText
// Abstract function for implementation
onSearchViewQueryTextChange(newText)
}
}
// clear the collapsing flag
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
}
.launchIn(viewScope)
val query = presenter.query
// Restoring a query the user had not submitted
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
searchItem.expandActionView()
searchView.setQuery(nonSubmittedQuery, false)
onSearchViewQueryTextChange(nonSubmittedQuery)
} else {
if (queryHint != null) {
searchView.queryHint = applicationContext?.getString(queryHint)
}
if (restoreCurrentQuery) {
// Restoring a query the user had submitted
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
onSearchViewQueryTextChange(query)
onSearchViewQueryTextSubmit(query)
}
}
}
// Workaround for weird behavior where searchView gets empty text change despite
// query being set already, prevents the query from being cleared
binding.root.post {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
}
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setCurrentSearchViewState(SearchViewState.FOCUSED)
} else {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
}
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onSearchMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
val localSearchView = searchItem.actionView as SearchView
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
if (localSearchView.toString().isNotBlank()) {
setCurrentSearchViewState(SearchViewState.COLLAPSING)
}
onSearchMenuItemActionCollapse(item)
return true
}
}
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Until everything is up and running don't accept empty queries
setCurrentSearchViewState(SearchViewState.LOADING)
}
private fun acceptEmptyQuery(): Boolean {
return when (currentSearchViewState) {
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
else -> false
}
}
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
// When loading ignore all requests other than loaded
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
return
}
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
// COLLAPSING -> LOADED)
if ((from != null) && (currentSearchViewState != from)) {
return
}
currentSearchViewState = to
}
/**
* Called by the SearchView since since the implementation of these can vary in subclasses
* Not abstract as they are optional
*/
protected open fun onSearchViewQueryTextChange(newText: String?) {
}
protected open fun onSearchViewQueryTextSubmit(query: String?) {
}
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
}
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
}
/**
* During the conversion to SearchableNucleusController (after which I plan to merge its code
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
* triggered
*/
override fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
false
} else {
true
}
}
}
@@ -12,11 +12,6 @@ open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope lateinit var presenterScope: CoroutineScope
/**
* Query from the view where applicable
*/
var query: String = ""
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
try { try {
super.onCreate(savedState) super.onCreate(savedState)
@@ -10,7 +10,6 @@ import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -59,11 +58,6 @@ open class ExtensionController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater) binding = ExtensionControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -110,8 +104,6 @@ open class ExtensionController :
override fun onButtonClick(position: Int) { override fun onButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
when (extension) { when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> { is Extension.Installed -> {
if (!extension.hasUpdate) { if (!extension.hasUpdate) {
openDetails(extension) openDetails(extension)
@@ -119,6 +111,12 @@ open class ExtensionController :
presenter.updateExtension(extension) presenter.updateExtension(extension)
} }
} }
is Extension.Available -> {
presenter.installExtension(extension)
}
is Extension.Untrusted -> {
openTrustDialog(extension)
}
} }
} }
@@ -149,11 +147,12 @@ open class ExtensionController :
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
when (extension) { if (extension is Extension.Installed) {
is Extension.Available -> presenter.installExtension(extension) openDetails(extension)
is Extension.Untrusted -> openTrustDialog(extension) } else if (extension is Extension.Untrusted) {
is Extension.Installed -> openDetails(extension) openTrustDialog(extension)
} }
return false return false
} }
@@ -22,7 +22,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -68,11 +67,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater) binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -2,10 +2,11 @@ package eu.kanade.tachiyomi.ui.browse.latest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
@@ -32,6 +33,10 @@ open class LatestController :
*/ */
protected var adapter: LatestAdapter? = null protected var adapter: LatestAdapter? = null
/*init {
setHasOptionsMenu(true)
}*/
/** /**
* Initiate the view with [R.layout.global_search_controller]. * Initiate the view with [R.layout.global_search_controller].
* *
@@ -41,11 +46,6 @@ open class LatestController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LatestControllerBinding.inflate(inflater) binding = LatestControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -82,6 +82,34 @@ open class LatestController :
onMangaClick(manga) onMangaClick(manga)
} }
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
/*inflater.inflate(R.menu.global_search, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})*/
}
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -30,6 +30,7 @@ import uy.kohesive.injekt.api.get
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class LatestPresenter( open class LatestPresenter(
private val sourcesToUse: List<CatalogueSource>? = null,
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
@@ -12,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -48,11 +47,6 @@ class PreMigrationController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PreMigrationControllerBinding.inflate(inflater) binding = PreMigrationControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -24,7 +24,7 @@ class MigratingManga(
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
var migrationStatus = MigrationStatus.RUNNING var migrationStatus: Int = MigrationStatus.RUNNING
@Volatile @Volatile
private var manga: Manga? = null private var manga: Manga? = null
@@ -42,3 +42,11 @@ class MigratingManga(
return MigrationProcessItem(this) return MigrationProcessItem(this)
} }
} }
class MigrationStatus {
companion object {
const val RUNNING = 0
const val MANGA_FOUND = 1
const val MANGA_NOT_FOUND = 2
}
}
@@ -16,7 +16,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -36,6 +35,7 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@@ -48,9 +48,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import timber.log.Timber import timber.log.Timber
@@ -75,7 +73,6 @@ class MigrationListController(bundle: Bundle? = null) :
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams) private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
private val migrationScope = CoroutineScope(Job() + Dispatchers.IO)
var migrationsJob: Job? = null var migrationsJob: Job? = null
private set private set
private var migratingManga: MutableList<MigratingManga>? = null private var migratingManga: MutableList<MigratingManga>? = null
@@ -86,11 +83,6 @@ class MigrationListController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationListControllerBinding.inflate(inflater) binding = MigrationListControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -107,7 +99,7 @@ class MigrationListController(bundle: Bundle? = null) :
val newMigratingManga = migratingManga ?: run { val newMigratingManga = migratingManga ?: run {
val new = config.mangaIds.map { val new = config.mangaIds.map {
MigratingManga(db, sourceManager, it, migrationScope.coroutineContext) MigratingManga(db, sourceManager, it, viewScope.coroutineContext + Dispatchers.IO)
} }
migratingManga = new.toMutableList() migratingManga = new.toMutableList()
new new
@@ -122,7 +114,7 @@ class MigrationListController(bundle: Bundle? = null) :
adapter?.updateDataSet(newMigratingManga.map { it.toModal() }) adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
if (migrationsJob == null) { if (migrationsJob == null) {
migrationsJob = migrationScope.launch { migrationsJob = viewScope.launchIO {
runMigrations(newMigratingManga) runMigrations(newMigratingManga)
} }
} }
@@ -283,7 +275,6 @@ class MigrationListController(bundle: Bundle? = null) :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
migrationScope.cancel()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
@@ -55,12 +55,14 @@ class MigrationProcessHolder(
binding.migrationMenu.setVectorCompat( binding.migrationMenu.setVectorCompat(
R.drawable.ic_more_vert_24dp, R.drawable.ic_more_vert_24dp,
view.context.getResourceColor(R.attr.colorOnPrimary) view.context
.getResourceColor(R.attr.colorOnPrimary)
) )
binding.skipManga.setVectorCompat( binding.skipManga.setVectorCompat(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
view.context.getResourceColor( view.context.getResourceColor(
R.attr.colorOnPrimary R
.attr.colorOnPrimary
) )
) )
binding.migrationMenu.isInvisible = true binding.migrationMenu.isInvisible = true
@@ -77,8 +79,7 @@ class MigrationProcessHolder(
true true
).withFadeTransaction() ).withFadeTransaction()
) )
} }.launchIn(adapter.controller.viewScope)
.launchIn(adapter.controller.viewScope)
} }
/*launchUI { /*launchUI {
@@ -114,8 +115,7 @@ class MigrationProcessHolder(
true true
).withFadeTransaction() ).withFadeTransaction()
) )
} }.launchIn(adapter.controller.viewScope)
.launchIn(adapter.controller.viewScope)
} else { } else {
binding.migrationMangaCardTo.loadingGroup.isVisible = false binding.migrationMangaCardTo.loadingGroup.isVisible = false
binding.migrationMangaCardTo.title.text = view.context.applicationContext binding.migrationMangaCardTo.title.text = view.context.applicationContext
@@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
enum class MigrationStatus {
RUNNING,
MANGA_FOUND,
MANGA_NOT_FOUND
}
@@ -6,7 +6,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -54,11 +53,6 @@ class MigrationMangaController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater) binding = MigrationMangaControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -5,12 +5,9 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@@ -27,37 +24,17 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SearchController( class SearchController(
private var manga: Manga? = null, private var manga: Manga? = null,
private var sources: List<CatalogueSource>? = null private var sources: List<CatalogueSource>? = null
) : GlobalSearchController( ) : GlobalSearchController(manga?.originalTitle) {
manga?.originalTitle,
bundle = bundleOf(
OLD_MANGA to manga?.id,
SOURCES to sources?.map { it.id }?.toLongArray()
)
) {
private var newManga: Manga? = null private var newManga: Manga? = null
private var progress = 1 private var progress = 1
var totalProgress = 0 var totalProgress = 0
constructor(mangaId: Long, sources: LongArray) :
this(
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking(),
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>()
)
@Suppress("unused")
constructor(bundle: Bundle) : this(
bundle.getLong(OLD_MANGA),
bundle.getLongArray(SOURCES) ?: LongArray(0)
)
/** /**
* Called when controller is initialized. * Called when controller is initialized.
*/ */
@@ -81,15 +58,31 @@ class SearchController(
) )
} }
fun migrateManga(manga: Manga, newManga: Manga) { override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(::manga.name, manga)
outState.putSerializable(::newManga.name, newManga)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
manga = savedInstanceState.getSerializable(::manga.name) as? Manga
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
}
fun migrateManga() {
val target = targetController as? MigrationInterface ?: return val target = targetController as? MigrationInterface ?: return
val manga = manga ?: return
val newManga = newManga ?: return
val nextManga = target.migrateManga(manga, newManga, true) val nextManga = target.migrateManga(manga, newManga, true)
replaceWithNewSearchController(nextManga) replaceWithNewSearchController(nextManga)
} }
fun copyManga(manga: Manga, newManga: Manga) { fun copyManga() {
val target = targetController as? MigrationInterface ?: return val target = targetController as? MigrationInterface ?: return
val manga = manga ?: return
val newManga = newManga ?: return
val nextManga = target.migrateManga(manga, newManga, false) val nextManga = target.migrateManga(manga, newManga, false)
replaceWithNewSearchController(nextManga) replaceWithNewSearchController(nextManga)
@@ -109,15 +102,14 @@ class SearchController(
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
if (targetController is MigrationListController) { if (targetController is MigrationListController) {
val migrationListController = targetController as? MigrationListController val migrationListController = targetController as? MigrationListController
val sourceManager = Injekt.get<SourceManager>() val sourceManager: SourceManager by injectLazy()
val source = sourceManager.get(manga.source) ?: return val source = sourceManager.get(manga.source) ?: return
migrationListController?.useMangaForMigration(manga, source) migrationListController?.useMangaForMigration(manga, source)
router.popCurrentController() router.popCurrentController()
return return
} }
newManga = manga newManga = manga
val dialog = val dialog = MigrationDialog()
MigrationDialog(this.manga ?: return, newManga ?: return, this)
dialog.targetController = this dialog.targetController = this
dialog.showDialog(router) dialog.showDialog(router)
} }
@@ -127,26 +119,12 @@ class SearchController(
super.onMangaClick(manga) super.onMangaClick(manga)
} }
class MigrationDialog(bundle: Bundle) : DialogController(bundle) { class MigrationDialog : DialogController() {
constructor(manga: Manga, newManga: Manga, callingController: Controller) : this(
bundleOf(
MANGA_KEY to manga,
NEW_MANGA_KEY to newManga
)
) {
this.callingController = callingController
}
private val manga: Manga = args.getSerializable(MANGA_KEY) as Manga
private val newManga: Manga = args.getSerializable(NEW_MANGA_KEY) as Manga
private var callingController: Controller? = null
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val prefValue = preferences.migrateFlags().get() val prefValue = preferences.migrateFlags().get()
val callingController = callingController
val preselected = val preselected =
MigrationFlags.getEnabledFlagsPositions( MigrationFlags.getEnabledFlagsPositions(
@@ -154,7 +132,7 @@ class SearchController(
) )
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(R.string.migration_dialog_what_to_include) .message(R.string.migration_dialog_what_to_include)
.listItemsMultiChoice( .listItemsMultiChoice(
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence }, items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
initialSelection = preselected.toIntArray() initialSelection = preselected.toIntArray()
@@ -167,27 +145,13 @@ class SearchController(
preferences.migrateFlags().set(newValue) preferences.migrateFlags().set(newValue)
} }
.positiveButton(R.string.migrate) { .positiveButton(R.string.migrate) {
if (callingController != null) { (targetController as? SearchController)?.migrateManga()
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.migrateManga(manga, newManga)
} }
.negativeButton(R.string.copy) { .negativeButton(R.string.copy) {
if (callingController != null) { (targetController as? SearchController)?.copyManga()
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.copyManga(manga, newManga)
} }
.neutralButton(android.R.string.cancel) .neutralButton(android.R.string.cancel)
} }
companion object {
const val MANGA_KEY = "manga_key"
const val NEW_MANGA_KEY = "new_manga_key"
}
} }
/** /**
@@ -219,15 +183,4 @@ class SearchController(
} }
.launchIn(viewScope) .launchIn(viewScope)
} }
override fun onTitleClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
router.pushController(SourceSearchController(manga!!, source, presenter.query).withFadeTransaction())
}
companion object {
const val OLD_MANGA = "old_manga"
const val SOURCES = "sources"
}
} }
@@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
class SourceSearchController(
bundle: Bundle
) : BrowseSourceController(bundle) {
constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf(
SOURCE_ID_KEY to source.id,
MANGA_KEY to manga,
SEARCH_QUERY_KEY to searchQuery
)
)
private var oldManga: Manga = args.getSerializable(MANGA_KEY) as Manga
private var newManga: Manga? = null
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
newManga = item.manga
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
val dialog =
SearchController.MigrationDialog(oldManga, newManga!!, this)
dialog.targetController = searchController
dialog.showDialog(router)
return true
}
private companion object {
const val MANGA_KEY = "oldManga"
}
}
@@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -44,11 +43,6 @@ class MigrationSourcesController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater) binding = MigrationSourcesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -28,14 +28,9 @@ class MigrationSourcesPresenter(
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> { private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader() val header = SelectionHeader()
return library return library.asSequence().map { it.source }.toSet()
.groupBy { it.source } .mapNotNull { if (it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */) sourceManager.getOrStub(it) else null }
.filterKeys { it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */ } .sortedBy { it.name.toLowerCase() }
.map { .map { SourceItem(it, header) }.toList()
val source = sourceManager.getOrStub(it.key)
SourceItem(source, it.value.size, header)
}
.sortedBy { it.source.name.toLowerCase() }
.toList()
} }
} }
@@ -26,8 +26,8 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
fun bind(item: SourceItem) { fun bind(item: SourceItem) {
val source = item.source val source = item.source
binding.title.text = "${source.name} (${item.mangaCount})" binding.title.text = source.name
binding.subtitle.isVisible = source.lang != "" binding.subtitle.isVisible = true
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
itemView.post { itemView.post {
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information. * @param source Instance of [Source] containing source information.
* @param header The header for this item. * @param header The header for this item.
*/ */
data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) : data class SourceItem(val source: Source, val header: SelectionHeader) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) { AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/** /**
@@ -10,13 +10,13 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
@@ -39,7 +39,12 @@ import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.ui.smartsearch.SmartSearchController import exh.ui.smartsearch.SmartSearchController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -50,7 +55,7 @@ import uy.kohesive.injekt.api.get
* [SourceAdapter.OnLatestClickListener] call function data on latest item click * [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/ */
class SourceController(bundle: Bundle? = null) : class SourceController(bundle: Bundle? = null) :
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(bundle), NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnSourceClickListener, SourceAdapter.OnSourceClickListener,
@@ -97,11 +102,6 @@ class SourceController(bundle: Bundle? = null) :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater) binding = SourceMainControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -203,7 +203,7 @@ class SourceController(bundle: Bundle? = null) :
items.add( items.add(
Pair( Pair(
activity.getString(R.string.categories), activity.getString(R.string.label_categories),
{ addToCategories(item.source) } { addToCategories(item.source) }
) )
) )
@@ -333,6 +333,44 @@ class SourceController(bundle: Bundle? = null) :
} }
// SY <-- // SY <--
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.source_main, menu)
// SY -->
if (mode == Mode.SMART_SEARCH) {
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_settings).isVisible = false
}
// SY <--
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(viewScope)
}
private fun performGlobalSearch(query: String) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
/** /**
* Called when an option menu item has been selected by the user. * Called when an option menu item has been selected by the user.
* *
@@ -393,29 +431,6 @@ class SourceController(bundle: Bundle? = null) :
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (mode == Mode.CATALOGUE) {
createOptionsMenu(
menu,
inflater,
R.menu.source_main,
R.id.action_search,
R.string.action_global_search_hint,
false // GlobalSearch handles the searching here
)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
// SY -->
if (mode == Mode.CATALOGUE) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
// SY <--
}
// SY --> // SY -->
@Parcelize @Parcelize
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
@@ -8,6 +8,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@@ -16,10 +17,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.input.input
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.elvishew.xlog.XLog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -37,7 +38,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController
@@ -56,22 +57,25 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.EmptyView
import exh.log.xLogW
import exh.md.similar.ui.EnableMangaDexSimilarDialogController import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class BrowseSourceController(bundle: Bundle) : open class BrowseSourceController(bundle: Bundle) :
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle), NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController, FabController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
@@ -385,11 +389,6 @@ open class BrowseSourceController(bundle: Bundle) :
actionFab?.shrinkOnScroll(recycler) actionFab?.shrinkOnScroll(recycler)
} }
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
@@ -402,8 +401,25 @@ open class BrowseSourceController(bundle: Bundle) :
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search) inflater.inflate(R.menu.source_browse, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { searchWithQuery(it.queryText.toString()) }
.launchIn(viewScope)
searchItem.fixExpand( searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() }, onExpand = { invalidateMenuOnExpand() },
@@ -434,10 +450,6 @@ open class BrowseSourceController(bundle: Bundle) :
// SY <-- // SY <--
} }
override fun onSearchViewQueryTextSubmit(query: String?) {
searchWithQuery(query ?: "")
}
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
@@ -530,8 +542,8 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) { /* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
// SY --> // SY -->
xLogW("> Failed to load next catalogue page!", error) XLog.tag("BrowseSourceController").enableStackTrace(2).w("> Failed to load next catalogue page!", error)
xLogW( XLog.tag("BrowseSourceController").enableStackTrace(2).w(
"> (source.id: %s, source.name: %s)", "> (source.id: %s, source.name: %s)",
presenter.source.id, presenter.source.id,
presenter.source.name presenter.source.name
@@ -81,6 +81,12 @@ open class BrowseSourcePresenter(
*/ */
lateinit var source: CatalogueSource lateinit var source: CatalogueSource
/**
* Query from the view.
*/
var query = searchQuery ?: ""
private set
/** /**
* Modifiable list of filters. * Modifiable list of filters.
*/ */
@@ -123,10 +129,6 @@ open class BrowseSourcePresenter(
private val filterSerializer = FilterSerializer() private val filterSerializer = FilterSerializer()
// SY <-- // SY <--
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@@ -7,6 +7,7 @@ import android.widget.AutoCompleteTextView
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.elvishew.xlog.XLog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -16,7 +17,6 @@ import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
import exh.log.xLogD
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() { open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
@@ -97,7 +97,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
addChipToGroup(name, holder) addChipToGroup(name, holder)
filter.state += name filter.state += name
} else { } else {
xLogD("Invalid tag: %s", name) XLog.tag("AutoComplete").d("Invalid tag: $name")
} }
} }
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -11,6 +10,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() { open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
@@ -25,9 +25,11 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.wrapper.hint = filter.name holder.wrapper.hint = filter.name
holder.edit.setText(filter.state) holder.edit.setText(filter.state)
holder.edit.doOnTextChanged { text, _, _, _ -> holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
filter.state = text.toString() override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
} filter.state = text.toString()
}
})
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -10,16 +10,20 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@@ -29,9 +33,8 @@ import uy.kohesive.injekt.injectLazy
*/ */
open class GlobalSearchController( open class GlobalSearchController(
protected val initialQuery: String? = null, protected val initialQuery: String? = null,
protected val extensionFilter: String? = null, protected val extensionFilter: String? = null
bundle: Bundle? = null ) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
GlobalSearchCardAdapter.OnMangaClickListener, GlobalSearchCardAdapter.OnMangaClickListener,
GlobalSearchAdapter.OnTitleClickListener { GlobalSearchAdapter.OnTitleClickListener {
@@ -42,11 +45,6 @@ open class GlobalSearchController(
*/ */
protected var adapter: GlobalSearchAdapter? = null protected var adapter: GlobalSearchAdapter? = null
/**
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
*/
private var optionsMenuSearchItem: MenuItem? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@@ -60,11 +58,6 @@ open class GlobalSearchController(
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = GlobalSearchControllerBinding.inflate(inflater) binding = GlobalSearchControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -107,32 +100,36 @@ open class GlobalSearchController(
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu( // Inflate menu.
menu, inflater.inflate(R.menu.global_search, menu)
inflater,
R.menu.global_search, // Initialize search menu
R.id.action_search, val searchItem = menu.findItem(R.id.action_search)
null, val searchView = searchItem.actionView as SearchView
false // the onMenuItemActionExpand will handle this searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
) )
optionsMenuSearchItem = menu.findItem(R.id.action_search) searchView.queryTextEvents()
} .filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach {
override fun onSearchMenuItemActionExpand(item: MenuItem?) { presenter.search(it.queryText.toString())
super.onSearchMenuItemActionExpand(item) searchItem.collapseActionView()
val searchView = optionsMenuSearchItem?.actionView as SearchView setTitle() // Update toolbar title
searchView.onActionViewExpanded() // Required to show the query in the view }
.launchIn(viewScope)
if (nonSubmittedQuery.isBlank()) {
searchView.setQuery(presenter.query, false)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
presenter.search(query ?: "")
optionsMenuSearchItem?.collapseActionView()
setTitle() // Update toolbar title
} }
/** /**
@@ -48,6 +48,12 @@ open class GlobalSearchPresenter(
*/ */
val sources by lazy { getSourcesToQuery() } val sources by lazy { getSourcesToQuery() }
/**
* Query from the view.
*/
var query = ""
private set
/** /**
* Fetches the different sources by user settings. * Fetches the different sources by user settings.
*/ */
@@ -0,0 +1,140 @@
package eu.kanade.tachiyomi.ui.browse.source.index
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.IndexAdapterBinding
/**
* Adapter that holds the search cards.
*
* @param controller instance of [IndexController].
*/
class IndexAdapter(val controller: IndexController) :
RecyclerView.Adapter<IndexAdapter.ViewHolder>() {
val clickListener: ClickListener = controller
private lateinit var binding: IndexAdapterBinding
var holder: IndexAdapter.ViewHolder? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexAdapter.ViewHolder {
binding = IndexAdapterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: IndexAdapter.ViewHolder, position: Int) {
this.holder = holder
holder.bindBrowse(null)
holder.bindLatest(null)
}
// stores and recycles views as they are scrolled off screen
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val latestAdapter = IndexCardAdapter(controller)
private var latestLastBoundResults: List<IndexCardItem>? = null
private val browseAdapter = IndexCardAdapter(controller)
private var browseLastBoundResults: List<IndexCardItem>? = null
init {
binding.browseBarWrapper.setOnClickListener {
clickListener.onBrowseClick()
}
binding.latestBarWrapper.setOnClickListener {
clickListener.onLatestClick()
}
binding.latestRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
binding.latestRecycler.adapter = latestAdapter
binding.browseRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
binding.browseRecycler.adapter = browseAdapter
}
fun bindLatest(latestResults: List<IndexCardItem>?) {
when {
latestResults == null -> {
binding.latestProgress.isVisible = true
showLatestResultsHolder()
}
latestResults.isEmpty() -> {
binding.latestProgress.isVisible = false
showLatestNoResults()
}
else -> {
binding.latestProgress.isVisible = false
showLatestResultsHolder()
}
}
if (latestResults !== latestLastBoundResults) {
latestAdapter.updateDataSet(latestResults)
latestLastBoundResults = latestResults
}
}
fun bindBrowse(browseResults: List<IndexCardItem>?) {
when {
browseResults == null -> {
binding.browseProgress.isVisible = true
showBrowseResultsHolder()
}
browseResults.isEmpty() -> {
binding.browseProgress.isVisible = false
showBrowseNoResults()
}
else -> {
binding.browseProgress.isVisible = false
showBrowseResultsHolder()
}
}
if (browseResults !== browseLastBoundResults) {
browseAdapter.updateDataSet(browseResults)
browseLastBoundResults = browseResults
}
}
private fun showLatestResultsHolder() {
binding.latestNoResultsFound.isVisible = false
}
private fun showLatestNoResults() {
binding.latestNoResultsFound.isVisible = true
}
private fun showBrowseResultsHolder() {
binding.browseNoResultsFound.isVisible = false
}
private fun showBrowseNoResults() {
binding.browseNoResultsFound.isVisible = true
}
fun setLatestImage(manga: Manga) {
latestAdapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
}
fun setBrowseImage(manga: Manga) {
browseAdapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
}
}
interface ClickListener {
fun onBrowseClick(search: String? = null, filters: String? = null)
fun onLatestClick()
}
override fun getItemCount(): Int = 1
}
@@ -6,29 +6,33 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.IndexControllerBinding import eu.kanade.tachiyomi.databinding.LatestControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
@@ -39,9 +43,10 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search * [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/ */
open class IndexController : open class IndexController :
SearchableNucleusController<IndexControllerBinding, IndexPresenter>, NucleusController<LatestControllerBinding, IndexPresenter>,
FabController, FabController,
IndexCardAdapter.OnMangaClickListener { IndexCardAdapter.OnMangaClickListener,
IndexAdapter.ClickListener {
constructor(source: CatalogueSource?) : super( constructor(source: CatalogueSource?) : super(
bundleOf( bundleOf(
@@ -60,10 +65,13 @@ open class IndexController :
var source: CatalogueSource? = null var source: CatalogueSource? = null
private var latestAdapter: IndexCardAdapter? = null /**
private var browseAdapter: IndexCardAdapter? = null * Adapter containing search results grouped by lang.
*/
protected var adapter: IndexAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/** /**
* Sheet containing filter items. * Sheet containing filter items.
@@ -82,7 +90,7 @@ open class IndexController :
* @return inflated view * @return inflated view
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = IndexControllerBinding.inflate(inflater) binding = LatestControllerBinding.inflate(inflater)
return binding.root return binding.root
} }
@@ -126,17 +134,35 @@ open class IndexController :
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(menu, inflater, R.menu.global_search, R.id.action_search) // Inflate menu.
} inflater.inflate(R.menu.global_search, menu)
override fun onSearchViewQueryTextSubmit(query: String?) { // Initialize search menu
onBrowseClick(query.nullIfBlank()) val searchItem = menu.findItem(R.id.action_search)
} val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
override fun onSearchViewQueryTextChange(newText: String?) { val query = presenter.query
if (router.backstack.lastOrNull()?.controller() == this) { if (query.isNotBlank()) {
presenter.query = newText ?: "" searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
} }
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@IndexController }
.onEach {
if (it is QueryTextEvent.QueryChanged) {
presenter.query = it.queryText.toString()
} else if (it is QueryTextEvent.QuerySubmitted) {
onBrowseClick(presenter.query.nullIfBlank())
}
}
.launchIn(viewScope)
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() }
)
} }
/** /**
@@ -150,39 +176,11 @@ open class IndexController :
// Prepare filter sheet // Prepare filter sheet
initFilterSheet() initFilterSheet()
latestAdapter = IndexCardAdapter(this) adapter = IndexAdapter(this)
binding.latestRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) // Create recycler and set adapter.
binding.latestRecycler.adapter = latestAdapter binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
browseAdapter = IndexCardAdapter(this)
binding.browseRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
binding.browseRecycler.adapter = browseAdapter
binding.latestBarWrapper.clicks()
.onEach {
onLatestClick()
}
.launchIn(viewScope)
binding.browseBarWrapper.clicks()
.onEach {
onBrowseClick()
}
.launchIn(viewScope)
presenter.latestItems
.onEach {
bind(it, true)
}
.launchIn(viewScope)
presenter.browseItems
.onEach {
bind(it, false)
}
.launchIn(viewScope)
presenter.getLatest() presenter.getLatest()
} }
@@ -263,64 +261,28 @@ open class IndexController :
override fun cleanupFab(fab: ExtendedFloatingActionButton) { override fun cleanupFab(fab: ExtendedFloatingActionButton) {
fab.setOnClickListener(null) fab.setOnClickListener(null)
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null actionFab = null
} }
private fun bind(results: List<IndexCardItem>?, isLatest: Boolean) { fun setLatestManga(results: List<IndexCardItem>?) {
val progress = if (isLatest) binding.latestProgress else binding.browseProgress adapter?.holder?.bindLatest(results)
when {
results == null -> {
progress.isVisible = true
showResultsHolder(isLatest)
}
results.isEmpty() -> {
progress.isVisible = false
showNoResults(isLatest)
}
else -> {
progress.isVisible = false
showResultsHolder(isLatest)
}
}
val adapter = if (isLatest) {
latestAdapter
} else {
browseAdapter
}
adapter?.updateDataSet(results)
} }
fun onError(e: Exception, isLatest: Boolean) { fun setBrowseManga(results: List<IndexCardItem>?) {
e.message?.let { adapter?.holder?.bindBrowse(results)
val textView = if (isLatest) {
binding.latestNoResultsFound
} else {
binding.browseNoResultsFound
}
textView.text = it
}
}
private fun showResultsHolder(isLatest: Boolean) {
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = false
}
private fun showNoResults(isLatest: Boolean) {
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = true
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
latestAdapter = null adapter = null
browseAdapter = null
super.onDestroyView(view) super.onDestroyView(view)
} }
fun onBrowseClick(search: String? = null, filters: String? = null) { override fun onBrowseClick(search: String?, filters: String?) {
router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction()) router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction())
} }
private fun onLatestClick() { override fun onLatestClick() {
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction()) router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
} }
@@ -330,14 +292,8 @@ open class IndexController :
* @param manga the initialized manga. * @param manga the initialized manga.
*/ */
fun onMangaInitialized(manga: Manga, isLatest: Boolean) { fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
val adapter = if (isLatest) latestAdapter else browseAdapter if (isLatest) adapter?.holder?.setLatestImage(manga)
adapter ?: return else adapter?.holder?.setBrowseImage(manga)
adapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (adapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
} }
companion object { companion object {
@@ -19,7 +19,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.JsonSavedSearch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -53,6 +52,11 @@ open class IndexPresenter(
*/ */
private var fetchSourcesSubscription: Subscription? = null private var fetchSourcesSubscription: Subscription? = null
/**
* Query from the view.
*/
var query = ""
/** /**
* Subject which fetches image of given manga. * Subject which fetches image of given manga.
*/ */
@@ -74,14 +78,6 @@ open class IndexPresenter(
*/ */
private var fetchImageSubscription: Subscription? = null private var fetchImageSubscription: Subscription? = null
val latestItems = MutableStateFlow<List<IndexCardItem>?>(null)
val browseItems = MutableStateFlow<List<IndexCardItem>?>(null)
init {
query = ""
}
override fun onDestroy() { override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe()
@@ -102,43 +98,54 @@ open class IndexPresenter(
initializeFetchImageSubscription() initializeFetchImageSubscription()
presenterScope.launch(Dispatchers.IO) { presenterScope.launch(Dispatchers.IO) {
if (latestItems.value != null) return@launch withUIContext {
val results = if (source.supportsLatest) { Observable.just(null).subscribeLatestCache({ view, results ->
try { view.setLatestManga(results)
})
}
if (source.supportsLatest) {
val results = try {
source.fetchLatestUpdates(1) source.fetchLatestUpdates(1)
.awaitSingle() .awaitSingle()
.mangas .mangas
.take(10)
.map { networkToLocalManga(it, source.id) } .map { networkToLocalManga(it, source.id) }
} catch (e: Exception) { } catch (e: Exception) {
withUIContext {
view?.onError(e, true)
}
emptyList() emptyList()
} }
} else emptyList() fetchImage(results, true)
fetchImage(results, true) withUIContext {
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
latestItems.value = results.map { IndexCardItem(it) } view.setLatestManga(results)
})
}
}
} }
presenterScope.launch(Dispatchers.IO) { presenterScope.launch(Dispatchers.IO) {
if (browseItems.value != null) return@launch withUIContext {
Observable.just(null).subscribeLatestCache({ view, results ->
view.setBrowseManga(results)
})
}
val results = try { val results = try {
source.fetchPopularManga(1) source.fetchPopularManga(1)
.awaitSingle() .awaitSingle()
.mangas .mangas
.take(10)
.map { networkToLocalManga(it, source.id) } .map { networkToLocalManga(it, source.id) }
} catch (e: Exception) { } catch (e: Exception) {
withUIContext {
view?.onError(e, true)
}
emptyList() emptyList()
} }
fetchImage(results, false) fetchImage(results, false)
browseItems.value = results.map { IndexCardItem(it) } withUIContext {
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
view.setBrowseManga(results)
})
}
} }
} }
@@ -214,10 +221,10 @@ open class IndexPresenter(
fun loadSearches(): List<EXHSavedSearch> { fun loadSearches(): List<EXHSavedSearch> {
val loaded = preferences.savedSearches().get() val loaded = preferences.savedSearches().get()
return loaded.mapNotNull { return loaded.map {
try { try {
val id = it.substringBefore(':').toLong() val id = it.substringBefore(':').toLong()
if (id != source.id) return@mapNotNull null if (id != source.id) return@map null
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':')) val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
val originalFilters = source.getFilterList() val originalFilters = source.getFilterList()
filterSerializer.deserialize(originalFilters, content.filters) filterSerializer.deserialize(originalFilters, content.filters)
@@ -232,6 +239,6 @@ open class IndexPresenter(
t.printStackTrace() t.printStackTrace()
null null
} }
} }.filterNotNull()
} }
} }
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -76,11 +75,6 @@ class CategoryController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -76,11 +75,6 @@ class BiometricTimesController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -5,9 +5,9 @@ import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.datetime.timePicker import com.afollestad.materialdialogs.datetime.timePicker
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import exh.log.xLogD
import java.util.Calendar import java.util.Calendar
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -48,9 +48,9 @@ class BiometricTimesCreateDialog<T>(bundle: Bundle? = null) : DialogController(b
.title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time) .title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
.timePicker(show24HoursView = false) { _, datetime -> .timePicker(show24HoursView = false) { _, datetime ->
val hour = datetime.get(Calendar.HOUR_OF_DAY) val hour = datetime.get(Calendar.HOUR_OF_DAY)
xLogD(hour) XLog.disableStackTrace().d(hour)
val minute = datetime.get(Calendar.MINUTE) val minute = datetime.get(Calendar.MINUTE)
xLogD(minute) XLog.disableStackTrace().d(minute)
if (hour !in 0..24 || minute !in 0..60) return@timePicker if (hour !in 0..24 || minute !in 0..60) return@timePicker
if (startTime != null) { if (startTime != null) {
endTime = hour.hours + minute.minutes endTime = hour.hours + minute.minutes
@@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.category.biometric package eu.kanade.tachiyomi.ui.category.biometric
import android.os.Bundle import android.os.Bundle
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import exh.log.xLogD
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import rx.Observable import rx.Observable
@@ -36,7 +36,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges -> preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges ->
timeRanges = prefTimeRanges.toList() timeRanges = prefTimeRanges.toList()
.mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { xLogD(it) } .mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { XLog.disableStackTrace().d(it) }
Observable.just(timeRanges) Observable.just(timeRanges)
.map { it.map(::BiometricTimesItem) } .map { it.map(::BiometricTimesItem) }
@@ -57,7 +57,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
return return
} }
xLogD(timeRange) XLog.disableStackTrace().d(timeRange)
preferences.biometricTimeRanges() += timeRange.toPreferenceString() preferences.biometricTimeRanges() += timeRange.toPreferenceString()
} }
@@ -13,7 +13,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -82,11 +81,6 @@ class SortTagController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -73,11 +72,6 @@ class RepoController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -79,6 +79,6 @@ class RepoPresenter(
} }
companion object { companion object {
val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex() val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex()
} }
} }
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -74,11 +73,6 @@ class SourceCategoryController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -10,7 +10,6 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@@ -57,11 +56,6 @@ class DownloadController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater) binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -14,7 +14,9 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
private var mangas = emptyList<Manga>() private var mangas = emptyList<Manga>()
private var categories = emptyList<Category>() private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>() private var preselected = emptyArray<Int>()
constructor( constructor(

Some files were not shown because too many files have changed in this diff Show More