Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d24bae841 | |||
| 5901509fbf | |||
| a8b07e0e05 | |||
| 808efd3968 | |||
| cedbbb05e4 | |||
| 84d22c11ee | |||
| 4cf068283b | |||
| e5fd460bb0 | |||
| 6d3095b503 | |||
| fcbe9590d3 | |||
| f7e5df2b6d | |||
| c58554ec75 | |||
| cdf2cf8a2d | |||
| 0922d3c288 | |||
| 505a8288be | |||
| b3baaa18d2 | |||
| 62e2b301c5 | |||
| 8b11357eff | |||
| 5bf4d5e434 | |||
| 45569947c4 | |||
| e9d25e9d32 | |||
| a03ed54c64 | |||
| cc499a7c07 | |||
| 0ca0a8f74f | |||
| 184aa4e211 | |||
| 8b7b4e05d2 | |||
| 501dedf845 | |||
| c6896d87d6 | |||
| 9af0d40479 | |||
| 1ed182853a | |||
| 1ef9717443 | |||
| afb80a23fc | |||
| 2bc380a9a3 | |||
| acc4d4a320 | |||
| ac8e5cf78c | |||
| 9464ae04aa | |||
| 1c61d37171 | |||
| b64a2cf816 | |||
| 9820e1097d | |||
| 153022df0a | |||
| 9e31806e5c | |||
| 3ec11cb81f | |||
| 960d67ec26 | |||
| 832107b932 | |||
| a575770be0 | |||
| a7979b8323 | |||
| e7cd7c06fa | |||
| 4cee1b3583 | |||
| dfa9b7462f | |||
| b456e38cc5 | |||
| b8e0b86df8 | |||
| c48f4770ee | |||
| 5191d7abb1 | |||
| 9da8a09cb4 | |||
| 98d5173507 | |||
| ff9fbc5265 | |||
| c721b90dc3 | |||
| 77ebecd87d | |||
| 518f2c1faa | |||
| 33f4c0ad08 | |||
| 8d0bfcd55e | |||
| 263c0fae8c | |||
| 7756f25312 | |||
| 6a0b523e86 | |||
| 070e2d94c7 | |||
| 743482dfd2 | |||
| f6b7f9e29f | |||
| 5c9f98bff1 | |||
| d375d7d8c8 | |||
| a88bcb0fa2 | |||
| 5512c6eb79 | |||
| 97e4b0e248 | |||
| 99a94150ea | |||
| 26b30adf4a | |||
| 4a115785eb | |||
| a8cb77cc7e | |||
| c44c37383d | |||
| 8e72394910 | |||
| e5349a3d33 | |||
| e6aa6f02e4 | |||
| 231c75df65 | |||
| 08c2bfd263 | |||
| 33bdf011b4 | |||
| 26deb46219 | |||
| 45bfd5f72c | |||
| 32d81eb1fa |
@@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.8.2)
|
- To the latest version of the app (stable is v1.8.4)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- 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
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ body:
|
|||||||
label: Tachiyomi version
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "1.8.2"
|
Example: "1.8.4"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: TAG - Bump version and push tag
|
- name: TAG - Bump version and push tag
|
||||||
uses: anothrNick/github-tag-action@1.17.2
|
uses: anothrNick/github-tag-action@1.39.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WITH_V: true
|
WITH_V: true
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
distribution: adopt
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
distribution: adopt
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 33
|
versionCode = 35
|
||||||
versionName = "1.8.2"
|
versionName = "1.8.4"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -202,6 +202,7 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
|
implementation(libs.markwon)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation(libs.bundles.conductor)
|
implementation(libs.bundles.conductor)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
@@ -74,7 +75,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(flags),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga),
|
backupExtensionInfo(databaseManga),
|
||||||
backupSavedSearches(),
|
backupSavedSearches(),
|
||||||
@@ -111,6 +112,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
file.openOutputStream().also {
|
file.openOutputStream().also {
|
||||||
// Force overwrite old file
|
// Force overwrite old file
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
@@ -149,10 +154,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*
|
*
|
||||||
* @return list of [BackupCategory] to be backed up
|
* @return list of [BackupCategory] to be backed up
|
||||||
*/
|
*/
|
||||||
private fun backupCategories(): List<BackupCategory> {
|
private fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
return databaseHelper.getCategories()
|
// Check if user wants category information in backup
|
||||||
.executeAsBlocking()
|
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
.map { BackupCategory.copyFrom(it) }
|
databaseHelper.getCategories()
|
||||||
|
.executeAsBlocking()
|
||||||
|
.map { BackupCategory.copyFrom(it) }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ fun getMergedMangaQuery() =
|
|||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga merged into a merged manga
|
||||||
|
*/
|
||||||
|
fun getMergedMangaForDownloadingQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ? AND ${Merged.COL_DOWNLOAD_CHAPTERS} = 1
|
||||||
|
) AS M
|
||||||
|
JOIN ${Manga.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get all the manga that are merged into other manga
|
* Query to get all the manga that are merged into other manga
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -187,16 +188,17 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param timeout duration after which to automatically dismiss the notification.
|
* @param timeout duration after which to automatically dismiss the notification.
|
||||||
* Only works on Android 8+.
|
* Only works on Android 8+.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String, timeout: Long? = null) {
|
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
timeout?.let { setTimeoutAfter(it) }
|
timeout?.let { setTimeoutAfter(it) }
|
||||||
|
contentIntent?.let { setContentIntent(it) }
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
@@ -11,6 +10,8 @@ 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.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
@@ -278,7 +279,8 @@ class Downloader(
|
|||||||
val maxDownloadsFromSource = queue
|
val maxDownloadsFromSource = queue
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.filterKeys { it !is UnmeteredSource }
|
.filterKeys { it !is UnmeteredSource }
|
||||||
.maxOf { it.value.size }
|
.maxOfOrNull { it.value.size }
|
||||||
|
?: 0
|
||||||
if (
|
if (
|
||||||
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||||
@@ -287,6 +289,7 @@ class Downloader(
|
|||||||
notifier.onWarning(
|
notifier.onWarning(
|
||||||
context.getString(R.string.download_queue_size_warning),
|
context.getString(R.string.download_queue_size_warning),
|
||||||
WARNING_NOTIF_TIMEOUT_MS,
|
WARNING_NOTIF_TIMEOUT_MS,
|
||||||
|
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,7 +477,7 @@ class Downloader(
|
|||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
||||||
|
|
||||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
return ImageUtil.getExtensionFromMimeType(mime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
import eu.kanade.tachiyomi.data.preference.*
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
Result.failure()
|
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||||
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (LibraryUpdateService.start(context)) {
|
return if (LibraryUpdateService.start(context)) {
|
||||||
@@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED })
|
||||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
|
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
@@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
|
||||||
return DEVICE_ONLY_ON_WIFI in restrictions
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
fun showQueueSizeWarningNotification() {
|
fun showQueueSizeWarningNotification() {
|
||||||
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
setContentTitle(context.getString(R.string.label_warning))
|
setContentTitle(context.getString(R.string.label_warning))
|
||||||
setContentText(context.getString(R.string.notification_size_warning))
|
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||||
|
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
@@ -341,6 +342,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -207,6 +208,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
// Despite what Android Studio
|
||||||
|
// states this can be null
|
||||||
ioScope?.cancel()
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
@@ -272,8 +275,7 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Adds list of manga to be updated.
|
* Adds list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param category the ID of the category to update, or -1 if no category specified.
|
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||||
* @param target the target to update.
|
|
||||||
*/
|
*/
|
||||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
@@ -308,17 +310,13 @@ class LibraryUpdateService(
|
|||||||
when (group) {
|
when (group) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
|
||||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||||
val statuses = loggedServices.associate {
|
|
||||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryManga.filter { manga ->
|
libraryManga.filter { manga ->
|
||||||
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
||||||
statuses[track.sync_id]?.get(track.status)
|
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||||
} ?: "not tracked"
|
} ?: TrackStatus.OTHER
|
||||||
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
status.int == trackingExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
@@ -357,12 +355,11 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
* Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
|
||||||
* to do heavy operations or network calls here.
|
* to do heavy operations or network calls here.
|
||||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
* progress.
|
* progress.
|
||||||
*
|
*
|
||||||
* @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() {
|
||||||
@@ -389,35 +386,38 @@ class LibraryUpdateService(
|
|||||||
return@async
|
return@async
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't continue to update if manga not in library
|
||||||
|
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
|
||||||
|
|
||||||
withUpdateNotification(
|
withUpdateNotification(
|
||||||
currentlyUpdatingManga,
|
currentlyUpdatingManga,
|
||||||
progressCount,
|
progressCount,
|
||||||
manga,
|
manga,
|
||||||
) { manga ->
|
) { mangaWithNotif ->
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
|
||||||
}
|
|
||||||
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
|
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
|
||||||
}
|
|
||||||
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
|
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
val (newChapters, _) = updateManga(manga, loggedServices)
|
val (newChapters, _) = updateManga(mangaWithNotif, loggedServices)
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
|
||||||
downloadChapters(manga, newChapters)
|
downloadChapters(mangaWithNotif, newChapters)
|
||||||
hasDownloads.set(true)
|
hasDownloads.set(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
newUpdates.add(
|
newUpdates.add(
|
||||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -436,11 +436,11 @@ class LibraryUpdateService(
|
|||||||
e.message
|
e.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(mangaWithNotif to errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.autoUpdateTrackers()) {
|
if (preferences.autoUpdateTrackers()) {
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(mangaWithNotif, loggedServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,13 +477,22 @@ class LibraryUpdateService(
|
|||||||
// We don't want to start downloading while the library is updating, because websites
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
// SY -->
|
// SY -->
|
||||||
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
val downloadingManga = db.getMergedMangasForDownloading(manga.id!!).executeAsBlocking()
|
||||||
.filterNot { it.downloadChapters }
|
.associateBy { it.id!! }
|
||||||
.mapNotNull { it.mangaId } + manga.id!!
|
chapters.groupBy { it.manga_id }
|
||||||
} else emptyList()
|
.forEach {
|
||||||
|
downloadManager.downloadChapters(
|
||||||
|
downloadingManga[it.key] ?: return@forEach,
|
||||||
|
chapters,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filterNot { it.manga_id in chapterFilter } /* SY <-- */, false)
|
downloadManager.downloadChapters(manga, chapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -495,6 +504,7 @@ class LibraryUpdateService(
|
|||||||
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
||||||
|
|
||||||
|
var networkSManga: SManga? = null
|
||||||
// Update manga details metadata
|
// Update manga details metadata
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
@@ -506,8 +516,7 @@ class LibraryUpdateService(
|
|||||||
sManga.thumbnail_url = manga.thumbnail_url
|
sManga.thumbnail_url = manga.thumbnail_url
|
||||||
}
|
}
|
||||||
|
|
||||||
manga.copyFrom(sManga)
|
networkSManga = sManga
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -532,7 +541,20 @@ class LibraryUpdateService(
|
|||||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||||
.map { it.toSChapter() }
|
.map { it.toSChapter() }
|
||||||
|
|
||||||
return syncChaptersWithSource(db, chapters, manga, source)
|
// Get manga from database to account for if it was removed
|
||||||
|
// from library or database
|
||||||
|
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
|
||||||
|
?: return Pair(emptyList(), emptyList())
|
||||||
|
|
||||||
|
// Copy into [dbManga] to retain favourite value
|
||||||
|
networkSManga?.let {
|
||||||
|
dbManga.copyFrom(it)
|
||||||
|
db.insertManga(dbManga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [dbmanga] was used so that manga data doesn't get overwritten
|
||||||
|
// incase manga gets new chapter
|
||||||
|
return syncChaptersWithSource(db, chapters, dbManga, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
@@ -555,16 +577,16 @@ class LibraryUpdateService(
|
|||||||
currentlyUpdatingManga,
|
currentlyUpdatingManga,
|
||||||
progressCount,
|
progressCount,
|
||||||
manga,
|
manga,
|
||||||
) { manga ->
|
) { mangaWithNotif ->
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(mangaWithNotif.source)?.let { source ->
|
||||||
try {
|
try {
|
||||||
val networkManga =
|
val networkManga =
|
||||||
source.getMangaDetails(manga.toMangaInfo())
|
source.getMangaDetails(mangaWithNotif.toMangaInfo())
|
||||||
val sManga = networkManga.toSManga()
|
val sManga = networkManga.toSManga()
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
|
||||||
sManga.thumbnail_url?.let {
|
sManga.thumbnail_url?.let {
|
||||||
manga.thumbnail_url = it
|
mangaWithNotif.thumbnail_url = it
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(mangaWithNotif).executeAsBlocking()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ignore errors and continue
|
// Ignore errors and continue
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ object Notifications {
|
|||||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
||||||
const val ID_LIBRARY_SKIPPED = -103
|
const val ID_LIBRARY_SKIPPED = -104
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||||
|
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||||
const val DEVICE_CHARGING = "ac"
|
const val DEVICE_CHARGING = "ac"
|
||||||
|
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
||||||
|
|
||||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||||
@@ -28,13 +30,14 @@ object PreferenceValues {
|
|||||||
enum class AppTheme(val titleResId: Int?) {
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
DEFAULT(R.string.label_default),
|
DEFAULT(R.string.label_default),
|
||||||
MONET(R.string.theme_monet),
|
MONET(R.string.theme_monet),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
LAVENDER(R.string.theme_lavender),
|
||||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
YOTSUBA(R.string.theme_yotsuba),
|
|
||||||
TAKO(R.string.theme_tako),
|
TAKO(R.string.theme_tako),
|
||||||
GREEN_APPLE(R.string.theme_greenapple),
|
|
||||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||||
YINYANG(R.string.theme_yinyang),
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
DARK_BLUE(null),
|
DARK_BLUE(null),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -212,11 +213,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
||||||
|
|
||||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||||
|
|
||||||
@@ -288,10 +289,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
fun downloadNewChapter() = flowPrefs.getBoolean("download_new", false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
|
||||||
|
|
||||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
@@ -23,16 +22,6 @@ class TrackManager(context: Context) {
|
|||||||
// SY --> Mangadex from Neko
|
// SY --> Mangadex from Neko
|
||||||
const val MDLIST = 60
|
const val MDLIST = 60
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
|
||||||
const val READING = 1
|
|
||||||
const val REPEATING = 2
|
|
||||||
const val PLAN_TO_READ = 3
|
|
||||||
const val PAUSED = 4
|
|
||||||
const val COMPLETED = 5
|
|
||||||
const val DROPPED = 6
|
|
||||||
const val OTHER = 7
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val mdList = MdList(context, MDLIST)
|
val mdList = MdList(context, MDLIST)
|
||||||
@@ -54,17 +43,4 @@ class TrackManager(context: Context) {
|
|||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLogged }
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
// SY -->
|
|
||||||
val trackMap by lazy {
|
|
||||||
mapOf(
|
|
||||||
context.getString(R.string.reading) to READING,
|
|
||||||
context.getString(R.string.repeating) to REPEATING,
|
|
||||||
context.getString(R.string.plan_to_read) to PLAN_TO_READ,
|
|
||||||
context.getString(R.string.paused) to PAUSED,
|
|
||||||
context.getString(R.string.completed) to COMPLETED,
|
|
||||||
context.getString(R.string.dropped) to DROPPED,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
import exh.md.utils.FollowStatus
|
||||||
|
|
||||||
|
enum class TrackStatus(val int: Int, @StringRes val res: Int) {
|
||||||
|
READING(1, R.string.reading),
|
||||||
|
REPEATING(2, R.string.repeating),
|
||||||
|
PLAN_TO_READ(3, R.string.plan_to_read),
|
||||||
|
PAUSED(4, R.string.on_hold),
|
||||||
|
COMPLETED(5, R.string.completed),
|
||||||
|
DROPPED(6, R.string.dropped),
|
||||||
|
OTHER(7, R.string.not_tracked);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseTrackerStatus(tracker: Int, status: Int): TrackStatus? {
|
||||||
|
return when (tracker) {
|
||||||
|
TrackManager.MDLIST -> {
|
||||||
|
when (FollowStatus.fromInt(status)) {
|
||||||
|
FollowStatus.UNFOLLOWED -> null
|
||||||
|
FollowStatus.READING -> READING
|
||||||
|
FollowStatus.COMPLETED -> COMPLETED
|
||||||
|
FollowStatus.ON_HOLD -> PAUSED
|
||||||
|
FollowStatus.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
FollowStatus.DROPPED -> DROPPED
|
||||||
|
FollowStatus.RE_READING -> REPEATING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.MYANIMELIST -> {
|
||||||
|
when (status) {
|
||||||
|
MyAnimeList.READING -> READING
|
||||||
|
MyAnimeList.COMPLETED -> COMPLETED
|
||||||
|
MyAnimeList.ON_HOLD -> PAUSED
|
||||||
|
MyAnimeList.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
MyAnimeList.DROPPED -> DROPPED
|
||||||
|
MyAnimeList.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.ANILIST -> {
|
||||||
|
when (status) {
|
||||||
|
Anilist.READING -> READING
|
||||||
|
Anilist.COMPLETED -> COMPLETED
|
||||||
|
Anilist.ON_HOLD -> PAUSED
|
||||||
|
Anilist.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Anilist.DROPPED -> DROPPED
|
||||||
|
Anilist.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.KITSU -> {
|
||||||
|
when (status) {
|
||||||
|
Kitsu.READING -> READING
|
||||||
|
Kitsu.COMPLETED -> COMPLETED
|
||||||
|
Kitsu.ON_HOLD -> PAUSED
|
||||||
|
Kitsu.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Kitsu.DROPPED -> DROPPED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.SHIKIMORI -> {
|
||||||
|
when (status) {
|
||||||
|
Shikimori.READING -> READING
|
||||||
|
Shikimori.COMPLETED -> COMPLETED
|
||||||
|
Shikimori.ON_HOLD -> PAUSED
|
||||||
|
Shikimori.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Shikimori.DROPPED -> DROPPED
|
||||||
|
Shikimori.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.BANGUMI -> {
|
||||||
|
when (status) {
|
||||||
|
Bangumi.READING -> READING
|
||||||
|
Bangumi.COMPLETED -> COMPLETED
|
||||||
|
Bangumi.ON_HOLD -> PAUSED
|
||||||
|
Bangumi.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Bangumi.DROPPED -> DROPPED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.KOMGA -> {
|
||||||
|
when (status) {
|
||||||
|
Komga.READING -> READING
|
||||||
|
Komga.COMPLETED -> COMPLETED
|
||||||
|
Komga.UNREAD -> null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,12 +55,13 @@ class AppUpdateChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && versionTag != syDebugVersion)
|
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && syDebugVersion == "0") || (syDebugVersion != "0" && versionTag != syDebugVersion)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun isNewVersion(versionTag: String): Boolean {
|
private fun isNewVersion(versionTag: String): Boolean {
|
||||||
// Removes prefixes like "r" or "v"
|
// Removes prefixes like "r" or "v"
|
||||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||||
|
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
return if (BuildConfig.DEBUG) {
|
return if (BuildConfig.DEBUG) {
|
||||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||||
@@ -69,7 +70,15 @@ class AppUpdateChecker {
|
|||||||
} else {
|
} else {
|
||||||
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
||||||
// tagged as something like "v0.1.2"
|
// tagged as something like "v0.1.2"
|
||||||
newVersion != BuildConfig.VERSION_NAME
|
val newSemVer = newVersion.split(".").map { it.toInt() }
|
||||||
|
val oldSemVer = oldVersion.split(".").map { it.toInt() }
|
||||||
|
|
||||||
|
oldSemVer.mapIndexed { index, i ->
|
||||||
|
if (newSemVer[index] > i) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
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 eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -22,21 +24,41 @@ internal class ExtensionGithubApi {
|
|||||||
private val networkService: NetworkHelper by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private var requiresFallbackSource = false
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val extensions = networkService.client
|
val githubResponse = if (requiresFallbackSource) null else try {
|
||||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
networkService.client
|
||||||
.await()
|
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||||
|
.await()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
|
||||||
|
requiresFallbackSource = true
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = githubResponse ?: run {
|
||||||
|
networkService.client
|
||||||
|
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensions = response
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions() /* SY --> */ + preferences.extensionRepos()
|
.toExtensions() /* SY --> */ + preferences.extensionRepos()
|
||||||
.get()
|
.get()
|
||||||
.flatMap { repoPath ->
|
.flatMap { repoPath ->
|
||||||
val url = "$BASE_URL$repoPath/repo/"
|
val url = if (requiresFallbackSource) {
|
||||||
|
"$FALLBACK_BASE_URL$repoPath@repo/"
|
||||||
|
} else {
|
||||||
|
"$BASE_URL$repoPath/repo/"
|
||||||
|
}
|
||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("${url}index.min.json"))
|
.newCall(GET("${url}index.min.json"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions(url)
|
.toExtensions(url, repoSource = true)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -85,7 +107,12 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExtensionJsonObject>.toExtensions(/* SY --> */ repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> {
|
private fun List<ExtensionJsonObject>.toExtensions(
|
||||||
|
// SY -->
|
||||||
|
repoUrl: String = getUrlPrefix(),
|
||||||
|
repoSource: Boolean = false,
|
||||||
|
// SY <--
|
||||||
|
): List<Extension.Available> {
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||||
@@ -106,6 +133,7 @@ internal class ExtensionGithubApi {
|
|||||||
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
|
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
|
||||||
// SY -->
|
// SY -->
|
||||||
repoUrl = repoUrl,
|
repoUrl = repoUrl,
|
||||||
|
isRepoSource = repoSource,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -125,6 +153,14 @@ internal class ExtensionGithubApi {
|
|||||||
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
|
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUrlPrefix(): String {
|
||||||
|
return if (requiresFallbackSource) {
|
||||||
|
FALLBACK_REPO_URL_PREFIX
|
||||||
|
} else {
|
||||||
|
REPO_URL_PREFIX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun Extension.isBlacklisted(
|
private fun Extension.isBlacklisted(
|
||||||
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
|
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
|
||||||
@@ -134,8 +170,10 @@ internal class ExtensionGithubApi {
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
private const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
private const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
|
||||||
|
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}tachiyomiorg/tachiyomi-extensions@repo/"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ExtensionJsonObject(
|
private data class ExtensionJsonObject(
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||||
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
"pm install-create --user current -i ${service.packageName} -S $size"
|
"pm install-create --user current -r -i ${service.packageName} -S $size"
|
||||||
} else {
|
} else {
|
||||||
"pm install-create -i ${service.packageName} -S $size"
|
"pm install-create -r -i ${service.packageName} -S $size"
|
||||||
}
|
}
|
||||||
val createResult = exec(createCommand)
|
val createResult = exec(createCommand)
|
||||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ sealed class Extension {
|
|||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
// SY -->
|
// SY -->
|
||||||
val repoUrl: String,
|
val repoUrl: String,
|
||||||
|
val isRepoSource: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
|
|||||||
const val PREF_DOH_GOOGLE = 2
|
const val PREF_DOH_GOOGLE = 2
|
||||||
const val PREF_DOH_ADGUARD = 3
|
const val PREF_DOH_ADGUARD = 3
|
||||||
const val PREF_DOH_QUAD9 = 4
|
const val PREF_DOH_QUAD9 = 4
|
||||||
|
const val PREF_DOH_ALIDNS = 5
|
||||||
|
const val PREF_DOH_DNSPOD = 6
|
||||||
|
const val PREF_DOH_360 = 7
|
||||||
|
const val PREF_DOH_QUAD101 = 8
|
||||||
|
|
||||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||||
DnsOverHttps.Builder().client(build())
|
DnsOverHttps.Builder().client(build())
|
||||||
@@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
|
|||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohAliDNS() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://dns.alidns.com/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("223.5.5.5"),
|
||||||
|
InetAddress.getByName("223.6.6.6"),
|
||||||
|
InetAddress.getByName("2400:3200::1"),
|
||||||
|
InetAddress.getByName("2400:3200:baba::1"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohDNSPod() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://doh.pub/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("1.12.12.12"),
|
||||||
|
InetAddress.getByName("120.53.53.53"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.doh360() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://doh.360.cn/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("101.226.4.6"),
|
||||||
|
InetAddress.getByName("218.30.118.6"),
|
||||||
|
InetAddress.getByName("123.125.81.6"),
|
||||||
|
InetAddress.getByName("140.207.198.6"),
|
||||||
|
InetAddress.getByName("180.163.249.75"),
|
||||||
|
InetAddress.getByName("101.199.113.208"),
|
||||||
|
InetAddress.getByName("36.99.170.86"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohQuad101() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("101.101.101.101"),
|
||||||
|
InetAddress.getByName("2001:de4::101"),
|
||||||
|
InetAddress.getByName("2001:de4::102"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
|||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.callTimeout(90, TimeUnit.SECONDS)
|
.callTimeout(2, TimeUnit.MINUTES)
|
||||||
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
@@ -46,6 +46,10 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
|||||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||||
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
||||||
|
PREF_DOH_ALIDNS -> builder.dohAliDNS()
|
||||||
|
PREF_DOH_DNSPOD -> builder.dohDNSPod()
|
||||||
|
PREF_DOH_360 -> builder.doh360()
|
||||||
|
PREF_DOH_QUAD101 -> builder.dohQuad101()
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder
|
return builder
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.SystemClock
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +37,11 @@ private class RateLimitInterceptor(
|
|||||||
private val rateLimitMillis = unit.toMillis(period)
|
private val rateLimitMillis = unit.toMillis(period)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
// Ignore canceled calls, otherwise they would jam the queue
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(requestQueue) {
|
synchronized(requestQueue) {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val waitTime = if (requestQueue.size < permits) {
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
@@ -51,6 +57,11 @@ private class RateLimitInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final check
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
if (requestQueue.size == permits) {
|
if (requestQueue.size == permits) {
|
||||||
requestQueue.removeAt(0)
|
requestQueue.removeAt(0)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
|
|||||||
private val host = httpUrl.host
|
private val host = httpUrl.host
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
if (chain.request().url.host != host) {
|
// Ignore canceled calls, otherwise they would jam the queue
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
} else if (chain.request().url.host != host) {
|
||||||
return chain.proceed(chain.request())
|
return chain.proceed(chain.request())
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(requestQueue) {
|
synchronized(requestQueue) {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val waitTime = if (requestQueue.size < permits) {
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
@@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final check
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
if (requestQueue.size == permits) {
|
if (requestQueue.size == permits) {
|
||||||
requestQueue.removeAt(0)
|
requestQueue.removeAt(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.junrar.Archive
|
import com.github.junrar.Archive
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@@ -18,16 +20,16 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.encodeToStream
|
import kotlinx.serialization.json.encodeToStream
|
||||||
import logcat.LogPriority
|
|
||||||
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
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@@ -35,50 +37,10 @@ import java.io.InputStream
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
class LocalSource(
|
||||||
|
private val context: Context,
|
||||||
companion object {
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
const val ID = 0L
|
) : CatalogueSource, UnmeteredSource {
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
|
||||||
val dir = getBaseDirectories(context).firstOrNull()
|
|
||||||
if (dir == null) {
|
|
||||||
input.close()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
|
||||||
if (cover == null) {
|
|
||||||
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
|
||||||
}
|
|
||||||
// It might not exist if using the external SD card
|
|
||||||
cover.parentFile?.mkdirs()
|
|
||||||
input.use {
|
|
||||||
cover.outputStream().use {
|
|
||||||
input.copyTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manga.thumbnail_url = cover.absolutePath
|
|
||||||
return cover
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns valid cover file inside [parent] directory.
|
|
||||||
*/
|
|
||||||
private fun getCoverFile(parent: File): File? {
|
|
||||||
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
|
||||||
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBaseDirectories(context: Context): List<File> {
|
|
||||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
|
||||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@@ -86,86 +48,100 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override val id = ID
|
override val name: String = context.getString(R.string.local_source)
|
||||||
override val name = context.getString(R.string.local_source)
|
|
||||||
override val lang = "other"
|
override val id: Long = ID
|
||||||
override val supportsLatest = true
|
|
||||||
|
override val lang: String = "other"
|
||||||
|
|
||||||
override fun toString() = name
|
override fun toString() = name
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
// Browse related
|
||||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||||
// SY -->
|
// SY -->
|
||||||
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
var mangaDirs = baseDirsFiles
|
||||||
var mangaDirs = baseDirs
|
// Filter out files that are hidden and is not a folder
|
||||||
.asSequence()
|
.filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
|
||||||
.mapNotNull { it.listFiles()?.toList() }
|
|
||||||
.flatten()
|
|
||||||
.filter { it.isDirectory }
|
|
||||||
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
|
|
||||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
when (state?.index) {
|
// Filter by query or last modified
|
||||||
0 -> {
|
mangaDirs = mangaDirs.filter {
|
||||||
mangaDirs = if (state.ascending) {
|
if (lastModifiedLimit == 0L) {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
it.name.contains(query, ignoreCase = true)
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
it.lastModified() >= lastModifiedLimit
|
||||||
}
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
mangaDirs = if (state.ascending) {
|
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
|
||||||
} else {
|
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is OrderBy -> {
|
||||||
|
when (filter.state!!.index) {
|
||||||
|
0 -> {
|
||||||
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
} else {
|
||||||
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
|
mangaDirs.sortedBy(File::lastModified)
|
||||||
|
} else {
|
||||||
|
mangaDirs.sortedByDescending(File::lastModified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> { /* Do nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform mangaDirs to list of SManga
|
||||||
val mangas = mangaDirs.map { mangaDir ->
|
val mangas = mangaDirs.map { mangaDir ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDir.name
|
title = mangaDir.name
|
||||||
url = mangaDir.name
|
url = mangaDir.name
|
||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
for (dir in baseDirs) {
|
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
if (cover != null && cover.exists()) {
|
||||||
if (cover != null && cover.exists()) {
|
thumbnail_url = cover.absolutePath
|
||||||
thumbnail_url = cover.absolutePath
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val sManga = this
|
// Fetch chapters of all the manga
|
||||||
val mangaInfo = this.toMangaInfo()
|
mangas.forEach { manga ->
|
||||||
runBlocking {
|
val mangaInfo = manga.toMangaInfo()
|
||||||
val chapters = getChapterList(mangaInfo)
|
runBlocking {
|
||||||
if (chapters.isNotEmpty()) {
|
val chapters = getChapterList(mangaInfo)
|
||||||
val chapter = chapters.last().toSChapter()
|
if (chapters.isNotEmpty()) {
|
||||||
val format = getFormat(chapter)
|
val chapter = chapters.last().toSChapter()
|
||||||
if (format is Format.Epub) {
|
val format = getFormat(chapter)
|
||||||
EpubFile(format.file).use { epub ->
|
|
||||||
epub.fillMangaMetadata(sManga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the cover from the first chapter found.
|
if (format is Format.Epub) {
|
||||||
if (thumbnail_url == null) {
|
EpubFile(format.file).use { epub ->
|
||||||
try {
|
epub.fillMangaMetadata(manga)
|
||||||
val dest = updateCover(chapter, sManga)
|
|
||||||
thumbnail_url = dest?.absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy the cover from the first chapter found if not available
|
||||||
|
if (manga.thumbnail_url == null) {
|
||||||
|
updateCover(chapter, manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,38 +176,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
// Manga details related
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val localDetails = getBaseDirectories(context)
|
var mangaInfo = manga
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||||
.flatten()
|
|
||||||
|
val coverFile = getCoverFile(manga.key, baseDirsFile)
|
||||||
|
|
||||||
|
coverFile?.let {
|
||||||
|
mangaInfo = mangaInfo.copy(cover = it.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val localDetails = getMangaDirsFiles(manga.key, baseDirsFile)
|
||||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||||
|
|
||||||
return if (localDetails != null) {
|
if (localDetails != null) {
|
||||||
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
||||||
|
|
||||||
manga.copy(
|
mangaInfo = mangaInfo.copy(
|
||||||
title = mangaJson.title ?: manga.title,
|
title = mangaJson.title ?: mangaInfo.title,
|
||||||
author = mangaJson.author ?: manga.author,
|
author = mangaJson.author ?: mangaInfo.author,
|
||||||
artist = mangaJson.artist ?: manga.artist,
|
artist = mangaJson.artist ?: mangaInfo.artist,
|
||||||
description = mangaJson.description ?: manga.description,
|
description = mangaJson.description ?: mangaInfo.description,
|
||||||
genres = mangaJson.genre ?: manga.genres,
|
genres = mangaJson.genre ?: mangaInfo.genres,
|
||||||
status = mangaJson.status ?: manga.status,
|
status = mangaJson.status ?: mangaInfo.status,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
manga
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mangaInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||||
val sManga = manga.toSManga()
|
val sManga = manga.toSManga()
|
||||||
|
|
||||||
val chapters = getBaseDirectories(context)
|
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||||
.asSequence()
|
return getMangaDirsFiles(manga.key, baseDirsFile)
|
||||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
// Only keep supported formats
|
||||||
.flatten()
|
|
||||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
@@ -243,14 +225,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
}
|
}
|
||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
|
|
||||||
|
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||||
|
|
||||||
val format = getFormat(chapterFile)
|
val format = getFormat(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillChapterMetadata(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { it.toChapterInfo() }
|
.map { it.toChapterInfo() }
|
||||||
@@ -259,12 +241,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
|
// Filters
|
||||||
|
override fun getFilterList() = FilterList(OrderBy(context))
|
||||||
|
|
||||||
|
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||||
|
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||||
|
|
||||||
|
private class OrderBy(context: Context) : Filter.Sort(
|
||||||
|
context.getString(R.string.local_filter_order_by),
|
||||||
|
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||||
|
Selection(0, true),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unused stuff
|
||||||
|
override suspend fun getPageList(chapter: ChapterInfo) = throw UnsupportedOperationException("Unused")
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
@@ -328,25 +322,89 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.also { coverCache.clearMemoryCache() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = POPULAR_FILTERS
|
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
|
|
||||||
private class OrderBy(context: Context) : Filter.Sort(
|
|
||||||
context.getString(R.string.local_filter_order_by),
|
|
||||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
|
||||||
Selection(0, true),
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class Format {
|
sealed class Format {
|
||||||
data class Directory(val file: File) : Format()
|
data class Directory(val file: File) : Format()
|
||||||
data class Zip(val file: File) : Format()
|
data class Zip(val file: File) : Format()
|
||||||
data class Rar(val file: File) : Format()
|
data class Rar(val file: File) : Format()
|
||||||
data class Epub(val file: File) : Format()
|
data class Epub(val file: File) : Format()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
|
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||||
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
|
||||||
|
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||||
|
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
|
return DiskUtil.getExternalStorages(context)
|
||||||
|
.map { File(it.absolutePath, localFolder) }
|
||||||
|
.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||||
|
return getBaseDirectories(context)
|
||||||
|
// Get all the files inside all baseDir
|
||||||
|
.flatMap { it.listFiles().orEmpty().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||||
|
return baseDirsFile
|
||||||
|
// Get the first mangaDir or null
|
||||||
|
.firstOrNull { it.isDirectory && it.name == mangaUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
|
||||||
|
return baseDirsFile
|
||||||
|
// Filter out ones that are not related to the manga and is not a directory
|
||||||
|
.filter { it.isDirectory && it.name == mangaUrl }
|
||||||
|
// Get all the files inside the filtered folders
|
||||||
|
.flatMap { it.listFiles().orEmpty().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||||
|
return getMangaDirsFiles(mangaUrl, baseDirsFile)
|
||||||
|
// Get all file whose names start with 'cover'
|
||||||
|
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||||
|
// Get the first actual image
|
||||||
|
.firstOrNull {
|
||||||
|
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
|
||||||
|
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||||
|
|
||||||
|
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
|
||||||
|
if (mangaDir == null) {
|
||||||
|
inputStream.close()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverFile = getCoverFile(manga.url, baseDirsFiles)
|
||||||
|
if (coverFile == null) {
|
||||||
|
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It might not exist at this point
|
||||||
|
coverFile.parentFile?.mkdirs()
|
||||||
|
inputStream.use { input ->
|
||||||
|
coverFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a .nomedia file
|
||||||
|
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||||
|
|
||||||
|
manga.thumbnail_url = coverFile.absolutePath
|
||||||
|
return coverFile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||||
|
|||||||
@@ -419,6 +419,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata
|
|||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE
|
||||||
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_VISIBILITY_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
||||||
@@ -688,6 +689,9 @@ class EHentai(
|
|||||||
uploader?.let {
|
uploader?.let {
|
||||||
tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL)
|
tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
|
visible?.let {
|
||||||
|
tags += RaisedTag(EH_VISIBILITY_NAMESPACE, it.substringAfter('(').substringBeforeLast(')'), TAG_TYPE_VIRTUAL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
||||||
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
||||||
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
||||||
private fun blockedUploaders() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty()
|
||||||
|
|
||||||
private val mangadexService by lazy {
|
private val mangadexService by lazy {
|
||||||
MangaDexService(client)
|
MangaDexService(client)
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
|||||||
import exh.log.xLogW
|
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 kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
@@ -63,18 +64,27 @@ class MergedSource : HttpSource() {
|
|||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val mergedManga = db.getManga(manga.key, id).executeAsBlocking() ?: throw Exception("merged manga not in db")
|
val mergedManga = db.getManga(manga.key, id).executeAsBlocking()
|
||||||
val mangaReferences = db.getMergedMangaReferences(mergedManga.id ?: throw Exception("merged manga id is null")).executeOnIO()
|
?: throw Exception("merged manga not in db")
|
||||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
val mangaReferences = db.getMergedMangaReferences(mergedManga.id!!).executeAsBlocking()
|
||||||
if (mangaReferences.size == 1 &&
|
.apply {
|
||||||
run {
|
if (isEmpty()) {
|
||||||
val mangaReference = mangaReferences.firstOrNull()
|
throw IllegalArgumentException(
|
||||||
mangaReference == null || mangaReference.mangaSourceId == MERGED_SOURCE_ID
|
"Manga references are empty, info unavailable, merge is likely corrupted",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (size == 1 && first().mangaSourceId == MERGED_SOURCE_ID) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Manga references contain only the merged reference, merge is likely corrupted",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
|
|
||||||
|
|
||||||
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga }
|
||||||
val dbManga = mangaInfoReference?.let { db.getManga(it.mangaUrl, it.mangaSourceId).executeOnIO()?.toMangaInfo() }
|
?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
||||||
|
val dbManga = mangaInfoReference?.run {
|
||||||
|
db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()?.toMangaInfo()
|
||||||
|
}
|
||||||
(dbManga ?: mergedManga.toMangaInfo()).copy(
|
(dbManga ?: mergedManga.toMangaInfo()).copy(
|
||||||
key = manga.key,
|
key = manga.key,
|
||||||
)
|
)
|
||||||
@@ -143,41 +153,50 @@ class MergedSource : HttpSource() {
|
|||||||
|
|
||||||
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
||||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
if (mangaReferences.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
||||||
|
val semaphore = Semaphore(5)
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
mangaReferences
|
mangaReferences
|
||||||
.map {
|
.groupBy(MergedMangaReference::mangaSourceId)
|
||||||
|
.minus(MERGED_SOURCE_ID)
|
||||||
|
.map { (_, values) ->
|
||||||
async {
|
async {
|
||||||
try {
|
semaphore.withPermit {
|
||||||
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null
|
values.map {
|
||||||
val (source, loadedManga, reference) =
|
try {
|
||||||
it.load(db, sourceManager)
|
val (source, loadedManga, reference) =
|
||||||
if (loadedManga != null && reference.getChapterUpdates) {
|
it.load(db, sourceManager)
|
||||||
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
if (loadedManga != null && reference.getChapterUpdates) {
|
||||||
.map { it.toSChapter() }
|
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
||||||
val results =
|
.map(ChapterInfo::toSChapter)
|
||||||
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
val results =
|
||||||
if (ifDownloadNewChapters && reference.downloadChapters) {
|
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
||||||
downloadManager.downloadChapters(
|
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||||
loadedManga,
|
downloadManager.downloadChapters(
|
||||||
results.first,
|
loadedManga,
|
||||||
)
|
results.first,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
results
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
exception = e
|
||||||
|
null
|
||||||
}
|
}
|
||||||
results
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
exception = e
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
|
.flatten()
|
||||||
.let { pairs ->
|
.let { pairs ->
|
||||||
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
|
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
|
||||||
}
|
}
|
||||||
@@ -187,7 +206,7 @@ class MergedSource : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource {
|
suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource {
|
||||||
var manga = db.getManga(mangaUrl, mangaSourceId).executeOnIO()
|
var manga = db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()
|
||||||
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
||||||
if (manga == null) {
|
if (manga == null) {
|
||||||
manga = Manga.create(mangaSourceId).apply {
|
manga = Manga.create(mangaSourceId).apply {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ interface ThemingDelegate {
|
|||||||
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_GreenApple
|
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||||
}
|
}
|
||||||
|
PreferenceValues.AppTheme.LAVENDER -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Lavender
|
||||||
|
}
|
||||||
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import coil.load
|
|||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionItemBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionItemBinding
|
||||||
import eu.kanade.tachiyomi.extension.api.REPO_URL_PREFIX
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
@@ -57,15 +56,14 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
// SY -->
|
// SY -->
|
||||||
private fun String.plusRepo(extension: Extension): String {
|
private fun String.plusRepo(extension: Extension): String {
|
||||||
return if (extension is Extension.Available) {
|
return if (extension is Extension.Available) {
|
||||||
when (extension.repoUrl) {
|
if (!extension.isRepoSource) {
|
||||||
REPO_URL_PREFIX -> this
|
this
|
||||||
else -> {
|
} else {
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this + " • "
|
"$this • "
|
||||||
} + itemView.context.getString(R.string.repo_source)
|
} + itemView.context.getString(R.string.repo_source)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else this
|
} else this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration
|
package eu.kanade.tachiyomi.ui.browse.migration
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
|
|
||||||
object MigrationFlags {
|
object MigrationFlags {
|
||||||
|
|
||||||
const val CHAPTERS = 0b0001
|
const val CHAPTERS = 0b00001
|
||||||
const val CATEGORIES = 0b0010
|
const val CATEGORIES = 0b00010
|
||||||
const val TRACK = 0b0100
|
const val TRACK = 0b00100
|
||||||
const val EXTRA = 0b1000
|
const val CUSTOM_COVER = 0b01000
|
||||||
|
const val EXTRA = 0b10000
|
||||||
private const val CHAPTERS2 = 0x1
|
|
||||||
private const val CATEGORIES2 = 0x2
|
|
||||||
private const val TRACK2 = 0x4
|
|
||||||
|
|
||||||
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track, R.string.log_extra)
|
|
||||||
|
|
||||||
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, EXTRA)
|
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
@@ -29,15 +20,11 @@ object MigrationFlags {
|
|||||||
return value and TRACK != 0
|
return value and TRACK != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasCustomCover(value: Int): Boolean {
|
||||||
|
return value and CUSTOM_COVER != 0
|
||||||
|
}
|
||||||
|
|
||||||
fun hasExtra(value: Int): Boolean {
|
fun hasExtra(value: Int): Boolean {
|
||||||
return value and EXTRA != 0
|
return value and EXTRA != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
|
||||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
|
||||||
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -62,11 +62,13 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
|
|||||||
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
||||||
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
||||||
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
||||||
|
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||||
|
|
||||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
|
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
|
|
||||||
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
||||||
@@ -93,6 +95,7 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
|
|||||||
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||||
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||||
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
||||||
|
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
||||||
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
||||||
preferences.migrateFlags().set(flags)
|
preferences.migrateFlags().set(flags)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|||||||
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
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.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
@@ -9,6 +10,7 @@ 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.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
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 kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -19,6 +21,7 @@ class MigrationProcessAdapter(
|
|||||||
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
var items: List<MigrationProcessItem> = emptyList()
|
var items: List<MigrationProcessItem> = emptyList()
|
||||||
|
|
||||||
@@ -148,11 +151,17 @@ class MigrationProcessAdapter(
|
|||||||
// Update track
|
// Update track
|
||||||
if (MigrationFlags.hasTracks(flags)) {
|
if (MigrationFlags.hasTracks(flags)) {
|
||||||
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
||||||
for (track in tracks) {
|
if (tracks.isNotEmpty()) {
|
||||||
track.id = null
|
tracks.forEach { track ->
|
||||||
track.manga_id = manga.id!!
|
track.id = null
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
}
|
||||||
|
db.insertTracks(tracks).executeAsBlocking()
|
||||||
}
|
}
|
||||||
db.insertTracks(tracks).executeAsBlocking()
|
}
|
||||||
|
// Update custom cover
|
||||||
|
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga).inputStream())
|
||||||
}
|
}
|
||||||
// Update extras
|
// Update extras
|
||||||
if (MigrationFlags.hasExtra(flags)) {
|
if (MigrationFlags.hasExtra(flags)) {
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
|||||||
else -> throw Exception("Unknown state")
|
else -> throw Exception("Unknown state")
|
||||||
},
|
},
|
||||||
)?.apply {
|
)?.apply {
|
||||||
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) {
|
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
|
||||||
view.context.getResourceColor(R.attr.colorAccent)
|
|
||||||
} else {
|
|
||||||
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
||||||
|
} else {
|
||||||
|
view.context.getResourceColor(R.attr.colorPrimary)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTint(color)
|
setTint(color)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
|||||||
view.popupMenu(
|
view.popupMenu(
|
||||||
menuRes = R.menu.download_single,
|
menuRes = R.menu.download_single,
|
||||||
initMenu = {
|
initMenu = {
|
||||||
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
|
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition > 1
|
||||||
findItem(R.id.move_to_bottom).isVisible =
|
findItem(R.id.move_to_bottom).isVisible =
|
||||||
bindingAdapterPosition != adapter.itemCount - 1
|
bindingAdapterPosition != adapter.itemCount - 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.view.doOnAttach
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
@@ -345,8 +344,10 @@ class LibraryController(
|
|||||||
onTabsSettingsChanged(firstLaunch = true)
|
onTabsSettingsChanged(firstLaunch = true)
|
||||||
|
|
||||||
// Delay the scroll position to allow the view to be properly measured.
|
// Delay the scroll position to allow the view to be properly measured.
|
||||||
view.doOnAttach {
|
view.post {
|
||||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
if (isAttached) {
|
||||||
|
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the manga map to child fragments after the adapter is updated.
|
// Send the manga map to child fragments after the adapter is updated.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
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.data.track.TrackStatus
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -42,7 +43,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Comparator
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -830,7 +830,7 @@ class LibraryPresenter(
|
|||||||
SManga.ONGOING to context.getString(R.string.ongoing),
|
SManga.ONGOING to context.getString(R.string.ongoing),
|
||||||
SManga.LICENSED to context.getString(R.string.licensed),
|
SManga.LICENSED to context.getString(R.string.licensed),
|
||||||
SManga.CANCELLED to context.getString(R.string.cancelled),
|
SManga.CANCELLED to context.getString(R.string.cancelled),
|
||||||
SManga.ON_HIATUS to context.getString(R.string.ongoing),
|
SManga.ON_HIATUS to context.getString(R.string.on_hiatus),
|
||||||
SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished),
|
SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished),
|
||||||
SManga.COMPLETED to context.getString(R.string.completed),
|
SManga.COMPLETED to context.getString(R.string.completed),
|
||||||
SManga.UNKNOWN to context.getString(R.string.unknown),
|
SManga.UNKNOWN to context.getString(R.string.unknown),
|
||||||
@@ -848,15 +848,9 @@ class LibraryPresenter(
|
|||||||
.let(grouping::putAll)
|
.let(grouping::putAll)
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
grouping.putAll(
|
grouping.putAll(
|
||||||
listOf(
|
TrackStatus.values()
|
||||||
TrackManager.READING to context.getString(R.string.reading),
|
.map { it.int to context.getString(it.res) }
|
||||||
TrackManager.REPEATING to context.getString(R.string.repeating),
|
.associateBy(Pair<Int, *>::first),
|
||||||
TrackManager.PLAN_TO_READ to context.getString(R.string.plan_to_read),
|
|
||||||
TrackManager.PAUSED to context.getString(R.string.on_hold),
|
|
||||||
TrackManager.COMPLETED to context.getString(R.string.completed),
|
|
||||||
TrackManager.DROPPED to context.getString(R.string.dropped),
|
|
||||||
TrackManager.OTHER to context.getString(R.string.not_tracked),
|
|
||||||
).associateBy(Pair<Int, *>::first),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,21 +859,12 @@ class LibraryPresenter(
|
|||||||
when (groupType) {
|
when (groupType) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||||
val statuses = loggedServices.associate {
|
|
||||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
|
||||||
}
|
|
||||||
libraryManga.forEach { libraryItem ->
|
libraryManga.forEach { libraryItem ->
|
||||||
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
||||||
statuses[track.sync_id]?.get(track.status)
|
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||||
} ?: "not tracked"
|
} ?: TrackStatus.OTHER
|
||||||
val group = grouping.values.find { (statusInt) ->
|
|
||||||
statusInt == (trackManager.trackMap[status] ?: TrackManager.OTHER)
|
map.getOrPut(status.int) { mutableListOf() } += libraryItem
|
||||||
}
|
|
||||||
if (group != null) {
|
|
||||||
map.getOrPut(group.first) { mutableListOf() } += libraryItem
|
|
||||||
} else {
|
|
||||||
map.getOrPut(7) { mutableListOf() } += libraryItem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
|
|||||||
@@ -1118,7 +1118,7 @@ class MangaController :
|
|||||||
chaptersHeader.setNumChapters(chapters.size)
|
chaptersHeader.setNumChapters(chapters.size)
|
||||||
|
|
||||||
val adapter = chaptersAdapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
adapter.updateDataSet(presenter.cleanChapterNames(chapters))
|
adapter.updateDataSet(chapters)
|
||||||
|
|
||||||
if (selectedChapters.isNotEmpty()) {
|
if (selectedChapters.isNotEmpty()) {
|
||||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||||
|
|||||||
@@ -781,17 +781,6 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanChapterNames(chapters: List<ChapterItem>): List<ChapterItem> {
|
|
||||||
chapters.forEach {
|
|
||||||
it.name = it.name
|
|
||||||
.trim()
|
|
||||||
.removePrefix(manga.title)
|
|
||||||
.trim(*CHAPTER_TRIM_CHARS)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the UI after applying the filters.
|
* Updates the UI after applying the filters.
|
||||||
*/
|
*/
|
||||||
@@ -1281,38 +1270,3 @@ class MangaPresenter(
|
|||||||
|
|
||||||
// Track sheet - end
|
// Track sheet - end
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
|
||||||
// Whitespace
|
|
||||||
' ',
|
|
||||||
'\u0009',
|
|
||||||
'\u000A',
|
|
||||||
'\u000B',
|
|
||||||
'\u000C',
|
|
||||||
'\u000D',
|
|
||||||
'\u0020',
|
|
||||||
'\u0085',
|
|
||||||
'\u00A0',
|
|
||||||
'\u1680',
|
|
||||||
'\u2000',
|
|
||||||
'\u2001',
|
|
||||||
'\u2002',
|
|
||||||
'\u2003',
|
|
||||||
'\u2004',
|
|
||||||
'\u2005',
|
|
||||||
'\u2006',
|
|
||||||
'\u2007',
|
|
||||||
'\u2008',
|
|
||||||
'\u2009',
|
|
||||||
'\u200A',
|
|
||||||
'\u2028',
|
|
||||||
'\u2029',
|
|
||||||
'\u202F',
|
|
||||||
'\u205F',
|
|
||||||
'\u3000',
|
|
||||||
// Separators
|
|
||||||
'-',
|
|
||||||
'_',
|
|
||||||
',',
|
|
||||||
':',
|
|
||||||
).toCharArray()
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.core.text.buildSpannedString
|
|||||||
import androidx.core.text.color
|
import androidx.core.text.color
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
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.databinding.ChaptersItemBinding
|
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
@@ -37,6 +38,8 @@ class ChapterHolder(
|
|||||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||||
}
|
}
|
||||||
else -> chapter.name
|
else -> chapter.name
|
||||||
|
// TODO: show cleaned name consistently around the app
|
||||||
|
// else -> cleanChapterName(chapter, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set correct text color
|
// Set correct text color
|
||||||
@@ -85,4 +88,47 @@ class ChapterHolder(
|
|||||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||||
binding.download.setState(item.status, item.progress)
|
binding.download.setState(item.status, item.progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
|
||||||
|
return chapter.name
|
||||||
|
.trim()
|
||||||
|
.removePrefix(manga.title)
|
||||||
|
.trim(*CHAPTER_TRIM_CHARS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||||
|
// Whitespace
|
||||||
|
' ',
|
||||||
|
'\u0009',
|
||||||
|
'\u000A',
|
||||||
|
'\u000B',
|
||||||
|
'\u000C',
|
||||||
|
'\u000D',
|
||||||
|
'\u0020',
|
||||||
|
'\u0085',
|
||||||
|
'\u00A0',
|
||||||
|
'\u1680',
|
||||||
|
'\u2000',
|
||||||
|
'\u2001',
|
||||||
|
'\u2002',
|
||||||
|
'\u2003',
|
||||||
|
'\u2004',
|
||||||
|
'\u2005',
|
||||||
|
'\u2006',
|
||||||
|
'\u2007',
|
||||||
|
'\u2008',
|
||||||
|
'\u2009',
|
||||||
|
'\u200A',
|
||||||
|
'\u2028',
|
||||||
|
'\u2029',
|
||||||
|
'\u202F',
|
||||||
|
'\u205F',
|
||||||
|
'\u3000',
|
||||||
|
|
||||||
|
// Separators
|
||||||
|
'-',
|
||||||
|
'_',
|
||||||
|
',',
|
||||||
|
':',
|
||||||
|
).toCharArray()
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class AboutController : SettingsController(), NoAppBarElevationController {
|
|||||||
is AppUpdateResult.NoNewUpdate -> {
|
is AppUpdateResult.NoNewUpdate -> {
|
||||||
activity?.toast(R.string.update_check_no_new_updates)
|
activity?.toast(R.string.update_check_no_new_updates)
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
activity?.toast(error.message)
|
activity?.toast(error.message)
|
||||||
|
|||||||
@@ -2,35 +2,58 @@ package eu.kanade.tachiyomi.ui.more
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
|
||||||
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
constructor(update: AppUpdateResult.NewUpdate) : this(
|
constructor(update: AppUpdateResult.NewUpdate) : this(
|
||||||
bundleOf(BODY_KEY to update.release.info, URL_KEY to update.release.getDownloadLink()),
|
bundleOf(
|
||||||
|
BODY_KEY to update.release.info,
|
||||||
|
RELEASE_URL_KEY to update.release.releaseLink,
|
||||||
|
DOWNLOAD_URL_KEY to update.release.getDownloadLink(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val releaseBody = args.getString(BODY_KEY)!!
|
||||||
|
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
|
||||||
|
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.setTitle(R.string.update_check_notification_update_available)
|
.setTitle(R.string.update_check_notification_update_available)
|
||||||
.setMessage(args.getString(BODY_KEY) ?: "")
|
.setMessage(info)
|
||||||
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
||||||
val appContext = applicationContext
|
applicationContext?.let { context ->
|
||||||
if (appContext != null) {
|
|
||||||
// Start download
|
// Start download
|
||||||
val url = args.getString(URL_KEY) ?: ""
|
val url = args.getString(DOWNLOAD_URL_KEY)!!
|
||||||
AppUpdateService.start(appContext, url)
|
AppUpdateService.start(context, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.update_check_ignore, null)
|
.setNeutralButton(R.string.update_check_open) { _, _ ->
|
||||||
|
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
|
||||||
|
}
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
|
||||||
|
// Make links in Markdown text clickable
|
||||||
|
(dialog?.findViewById(android.R.id.message) as? TextView)?.movementMethod =
|
||||||
|
LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val BODY_KEY = "NewUpdateDialogController.body"
|
private const val BODY_KEY = "NewUpdateDialogController.body"
|
||||||
private const val URL_KEY = "NewUpdateDialogController.key"
|
private const val RELEASE_URL_KEY = "NewUpdateDialogController.release_url"
|
||||||
|
private const val DOWNLOAD_URL_KEY = "NewUpdateDialogController.download_url"
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
@@ -99,16 +101,14 @@ import exh.source.isEhBasedSource
|
|||||||
import exh.util.defaultReaderType
|
import exh.util.defaultReaderType
|
||||||
import exh.util.floor
|
import exh.util.floor
|
||||||
import exh.util.mangaType
|
import exh.util.mangaType
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
@@ -165,8 +165,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
// SY -->
|
// SY -->
|
||||||
private var ehUtilsVisible = false
|
private var ehUtilsVisible = false
|
||||||
|
|
||||||
private val autoScrollFlow = MutableSharedFlow<Unit>()
|
|
||||||
private var autoScrollJob: Job? = null
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
private var lastShiftDoubleState: Boolean? = null
|
private var lastShiftDoubleState: Boolean? = null
|
||||||
@@ -264,19 +262,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
|
binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAutoscroll(interval: Double) {
|
|
||||||
autoScrollJob?.cancel()
|
|
||||||
if (interval == -1.0) return
|
|
||||||
|
|
||||||
val duration = interval.seconds
|
|
||||||
autoScrollJob = lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
while (true) {
|
|
||||||
delay(duration)
|
|
||||||
autoScrollFlow.emit(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,10 +276,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
readingModeToast?.cancel()
|
readingModeToast?.cancel()
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
// SY -->
|
|
||||||
autoScrollJob?.cancel()
|
|
||||||
autoScrollJob = null
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,6 +305,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
presenter.saveProgress()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
||||||
* Helps with rotations.
|
* Helps with rotations.
|
||||||
@@ -716,31 +702,34 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
binding.ehAutoscroll.checkedChanges()
|
binding.ehAutoscroll.checkedChanges()
|
||||||
.onEach {
|
.combine(binding.ehAutoscrollFreq.textChanges()) { checked, text ->
|
||||||
setupAutoscroll(
|
checked to text
|
||||||
if (it) {
|
|
||||||
preferences.autoscrollInterval().get().toDouble()
|
|
||||||
} else {
|
|
||||||
-1.0
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.mapLatest { (checked, text) ->
|
||||||
|
val parsed = text.toString().toDoubleOrNull()
|
||||||
binding.ehAutoscrollFreq.textChanges()
|
|
||||||
.onEach {
|
|
||||||
val parsed = it.toString().toDoubleOrNull()
|
|
||||||
|
|
||||||
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
||||||
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
|
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
|
||||||
preferences.autoscrollInterval().set(-1f)
|
preferences.autoscrollInterval().set(-1f)
|
||||||
binding.ehAutoscroll.isEnabled = false
|
binding.ehAutoscroll.isEnabled = false
|
||||||
setupAutoscroll(-1.0)
|
|
||||||
} else {
|
} else {
|
||||||
binding.ehAutoscrollFreq.error = null
|
binding.ehAutoscrollFreq.error = null
|
||||||
preferences.autoscrollInterval().set(parsed.toFloat())
|
preferences.autoscrollInterval().set(parsed.toFloat())
|
||||||
binding.ehAutoscroll.isEnabled = true
|
binding.ehAutoscroll.isEnabled = true
|
||||||
setupAutoscroll(if (binding.ehAutoscroll.isChecked) parsed else -1.0)
|
if (checked) {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
val interval = parsed.seconds
|
||||||
|
while (true) {
|
||||||
|
delay(interval)
|
||||||
|
viewer.let { v ->
|
||||||
|
when (v) {
|
||||||
|
is PagerViewer -> v.moveToNext()
|
||||||
|
is WebtoonViewer -> v.scrollDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
@@ -844,15 +833,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
autoScrollFlow
|
|
||||||
.onEach {
|
|
||||||
viewer.let { v ->
|
|
||||||
if (v is PagerViewer) v.moveToNext()
|
|
||||||
else if (v is WebtoonViewer) v.scrollDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exhCurrentpage(): ReaderPage? {
|
private fun exhCurrentpage(): ReaderPage? {
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.ui.reader
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
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
|
||||||
@@ -29,6 +27,7 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
|
|||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
@@ -64,6 +63,7 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -425,6 +425,14 @@ class ReaderPresenter(
|
|||||||
* that the user doesn't have to wait too long to continue reading.
|
* that the user doesn't have to wait too long to continue reading.
|
||||||
*/
|
*/
|
||||||
private fun preload(chapter: ReaderChapter) {
|
private fun preload(chapter: ReaderChapter) {
|
||||||
|
if (chapter.pageLoader is HttpPageLoader) {
|
||||||
|
val manga = manga ?: return
|
||||||
|
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga)
|
||||||
|
if (isDownloaded) {
|
||||||
|
chapter.state = ReaderChapter.State.Wait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -549,6 +557,10 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveProgress() {
|
||||||
|
getCurrentChapter()?.let { onChapterChanged(it) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the activity to preload the given [chapter].
|
* Called from the activity to preload the given [chapter].
|
||||||
*/
|
*/
|
||||||
@@ -761,7 +773,7 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImages(
|
private fun saveImages(
|
||||||
page1: ReaderPage,
|
page1: ReaderPage,
|
||||||
page2: ReaderPage,
|
page2: ReaderPage,
|
||||||
isLTR: Boolean,
|
isLTR: Boolean,
|
||||||
@@ -773,11 +785,8 @@ class ReaderPresenter(
|
|||||||
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
|
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
|
||||||
val stream2 = page2.stream!!
|
val stream2 = page2.stream!!
|
||||||
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
|
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
|
||||||
val imageBytes = stream1().readBytes()
|
val imageBitmap = ImageDecoder.newInstance(stream1())?.decode()!!
|
||||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
val imageBitmap2 = ImageDecoder.newInstance(stream2())?.decode()!!
|
||||||
|
|
||||||
val imageBytes2 = stream2().readBytes()
|
|
||||||
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
|
||||||
|
|
||||||
val chapter = page1.chapter.chapter
|
val chapter = page1.chapter.chapter
|
||||||
|
|
||||||
@@ -872,20 +881,22 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
Observable
|
Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (manga.isLocal()) {
|
stream().use {
|
||||||
val context = Injekt.get<Application>()
|
if (manga.isLocal()) {
|
||||||
LocalSource.updateCover(context, manga, stream())
|
val context = Injekt.get<Application>()
|
||||||
manga.updateCoverLastModified(db)
|
LocalSource.updateCover(context, manga, it)
|
||||||
R.string.cover_updated
|
|
||||||
SetAsCoverResult.Success
|
|
||||||
} else {
|
|
||||||
if (manga.favorite) {
|
|
||||||
coverCache.setCustomCoverToCache(manga, stream())
|
|
||||||
manga.updateCoverLastModified(db)
|
manga.updateCoverLastModified(db)
|
||||||
coverCache.clearMemoryCache()
|
coverCache.clearMemoryCache()
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
if (manga.favorite) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, it)
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
|
coverCache.clearMemoryCache()
|
||||||
|
SetAsCoverResult.Success
|
||||||
|
} else {
|
||||||
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
|
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
|
||||||
is ChapterTransition.Next -> bindNextChapterTransition(transition)
|
is ChapterTransition.Next -> bindNextChapterTransition(transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
missingChapterWarning(transition)
|
missingChapterWarning(transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -24,6 +23,7 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -366,13 +366,17 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
val imageBytes = imageStream.readBytes()
|
val imageBytes = imageStream.readBytes()
|
||||||
val imageBitmap = try {
|
val imageBitmap = try {
|
||||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
ImageDecoder.newInstance(imageBytes.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (imageBitmap == null) {
|
||||||
imageStream2.close()
|
imageStream2.close()
|
||||||
imageStream.close()
|
imageStream.close()
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageBytes.inputStream()
|
||||||
}
|
}
|
||||||
viewer.scope.launchUI { progressIndicator.setProgress(96) }
|
viewer.scope.launchUI { progressIndicator.setProgress(96) }
|
||||||
@@ -389,14 +393,18 @@ class PagerPageHolder(
|
|||||||
|
|
||||||
val imageBytes2 = imageStream2.readBytes()
|
val imageBytes2 = imageStream2.readBytes()
|
||||||
val imageBitmap2 = try {
|
val imageBitmap2 = try {
|
||||||
BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
ImageDecoder.newInstance(imageBytes2.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (imageBitmap2 == null) {
|
||||||
imageStream2.close()
|
imageStream2.close()
|
||||||
imageStream.close()
|
imageStream.close()
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageBytes.inputStream()
|
||||||
}
|
}
|
||||||
viewer.scope.launchUI { progressIndicator.setProgress(97) }
|
viewer.scope.launchUI { progressIndicator.setProgress(97) }
|
||||||
|
|||||||
@@ -67,9 +67,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
if (value) {
|
if (value) {
|
||||||
awaitingIdleViewerChapters?.let {
|
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||||
setChaptersDoubleShift(it)
|
setChaptersDoubleShift(viewerChapters)
|
||||||
awaitingIdleViewerChapters = null
|
awaitingIdleViewerChapters = null
|
||||||
|
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||||
|
adapter.nextTransition?.to?.let {
|
||||||
|
activity.requestPreloadChapter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ class WebtoonTransitionHolder(
|
|||||||
layout.orientation = LinearLayout.VERTICAL
|
layout.orientation = LinearLayout.VERTICAL
|
||||||
layout.gravity = Gravity.CENTER
|
layout.gravity = Gravity.CENTER
|
||||||
|
|
||||||
val paddingVertical = 48.dpToPx
|
val paddingVertical = 128.dpToPx
|
||||||
val paddingHorizontal = 32.dpToPx
|
val paddingHorizontal = 32.dpToPx
|
||||||
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
activity.requestPreloadChapter(firstItem.to)
|
activity.requestPreloadChapter(firstItem.to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
|
||||||
|
val lastItem = adapter.items.getOrNull(lastIndex)
|
||||||
|
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
|
||||||
|
activity.showMenu()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -223,9 +229,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
if (toChapter != null) {
|
if (toChapter != null) {
|
||||||
logcat { "Request preload destination chapter because we're on the transition" }
|
logcat { "Request preload destination chapter because we're on the transition" }
|
||||||
activity.requestPreloadChapter(toChapter)
|
activity.requestPreloadChapter(toChapter)
|
||||||
} else if (transition is ChapterTransition.Next) {
|
|
||||||
// No more chapters, show menu because the user is probably going to close the reader
|
|
||||||
activity.showMenu()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +255,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
logcat { "moveToPage" }
|
logcat { "moveToPage" }
|
||||||
val position = adapter.items.indexOf(page)
|
val position = adapter.items.indexOf(page)
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
recycler.scrollToPosition(position)
|
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||||
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
||||||
onScrolled(pos = position)
|
onScrolled(pos = position)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.webkit.WebStorage
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -20,9 +22,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
|
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
|
||||||
@@ -49,7 +55,9 @@ import eu.kanade.tachiyomi.util.preference.titleRes
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.debug.SettingsDebugController
|
import exh.debug.SettingsDebugController
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
@@ -57,10 +65,12 @@ 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
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import logcat.LogPriority
|
||||||
import rikka.sui.Sui
|
import rikka.sui.Sui
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
class SettingsAdvancedController : SettingsController() {
|
class SettingsAdvancedController : SettingsController() {
|
||||||
@@ -87,7 +97,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
key = Keys.verboseLogging
|
key = Keys.verboseLogging
|
||||||
titleRes = R.string.pref_verbose_logging
|
titleRes = R.string.pref_verbose_logging
|
||||||
summaryRes = R.string.pref_verbose_logging_summary
|
summaryRes = R.string.pref_verbose_logging_summary
|
||||||
defaultValue = false
|
defaultValue = isDevFlavor
|
||||||
|
|
||||||
onChange {
|
onChange {
|
||||||
activity?.toast(R.string.requires_app_restart)
|
activity?.toast(R.string.requires_app_restart)
|
||||||
@@ -170,6 +180,12 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
activity?.toast(R.string.cookies_cleared)
|
activity?.toast(R.string.cookies_cleared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
key = "pref_clear_webview_data"
|
||||||
|
titleRes = R.string.pref_clear_webview_data
|
||||||
|
|
||||||
|
onClick { clearWebViewData() }
|
||||||
|
}
|
||||||
intListPreference {
|
intListPreference {
|
||||||
key = Keys.dohProvider
|
key = Keys.dohProvider
|
||||||
titleRes = R.string.pref_dns_over_https
|
titleRes = R.string.pref_dns_over_https
|
||||||
@@ -179,6 +195,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
"Google",
|
"Google",
|
||||||
"AdGuard",
|
"AdGuard",
|
||||||
"Quad9",
|
"Quad9",
|
||||||
|
"AliDNS",
|
||||||
|
"DNSPod",
|
||||||
|
"360",
|
||||||
|
"Quad 101",
|
||||||
)
|
)
|
||||||
entryValues = arrayOf(
|
entryValues = arrayOf(
|
||||||
"-1",
|
"-1",
|
||||||
@@ -186,6 +206,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
PREF_DOH_GOOGLE.toString(),
|
PREF_DOH_GOOGLE.toString(),
|
||||||
PREF_DOH_ADGUARD.toString(),
|
PREF_DOH_ADGUARD.toString(),
|
||||||
PREF_DOH_QUAD9.toString(),
|
PREF_DOH_QUAD9.toString(),
|
||||||
|
PREF_DOH_ALIDNS.toString(),
|
||||||
|
PREF_DOH_DNSPOD.toString(),
|
||||||
|
PREF_DOH_360.toString(),
|
||||||
|
PREF_DOH_QUAD101.toString(),
|
||||||
)
|
)
|
||||||
defaultValue = "-1"
|
defaultValue = "-1"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
@@ -486,11 +510,30 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearWebViewData() {
|
||||||
|
if (activity == null) return
|
||||||
|
try {
|
||||||
|
val webview = WebView(activity!!)
|
||||||
|
webview.setDefaultSettings()
|
||||||
|
webview.clearCache(true)
|
||||||
|
webview.clearFormData()
|
||||||
|
webview.clearHistory()
|
||||||
|
webview.clearSslPreferences()
|
||||||
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||||
|
activity?.toast(R.string.webview_data_deleted)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
activity?.toast(R.string.cache_delete_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
// SY -->
|
// SY -->
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|||||||
@@ -125,20 +125,20 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
titleRes = R.string.pref_category_auto_download
|
titleRes = R.string.pref_category_auto_download
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
bindTo(preferences.downloadNew())
|
bindTo(preferences.downloadNewChapter())
|
||||||
titleRes = R.string.pref_download_new
|
titleRes = R.string.pref_download_new
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
bindTo(preferences.downloadNewCategories())
|
bindTo(preferences.downloadNewChapterCategories())
|
||||||
titleRes = R.string.categories
|
titleRes = R.string.categories
|
||||||
onClick {
|
onClick {
|
||||||
DownloadCategoriesDialog().showDialog(router)
|
DownloadCategoriesDialog().showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleIf(preferences.downloadNew()) { it }
|
visibleIf(preferences.downloadNewChapter()) { it }
|
||||||
|
|
||||||
fun updateSummary() {
|
fun updateSummary() {
|
||||||
val selectedCategories = preferences.downloadNewCategories().get()
|
val selectedCategories = preferences.downloadNewChapterCategories().get()
|
||||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
val includedItemsText = if (selectedCategories.isEmpty()) {
|
val includedItemsText = if (selectedCategories.isEmpty()) {
|
||||||
@@ -147,7 +147,7 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
selectedCategories.joinToString { it.name }
|
selectedCategories.joinToString { it.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
|
val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
|
||||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
||||||
@@ -163,10 +163,10 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.downloadNewCategories().asFlow()
|
preferences.downloadNewChapterCategories().asFlow()
|
||||||
.onEach { updateSummary() }
|
.onEach { updateSummary() }
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
preferences.downloadNewCategoriesExclude().asFlow()
|
preferences.downloadNewChapterCategoriesExclude().asFlow()
|
||||||
.onEach { updateSummary() }
|
.onEach { updateSummary() }
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
}
|
}
|
||||||
@@ -254,8 +254,8 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
var selected = categories
|
var selected = categories
|
||||||
.map {
|
.map {
|
||||||
when (it.id.toString()) {
|
when (it.id.toString()) {
|
||||||
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||||
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||||
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,8 +282,8 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
.map { categories[it].id.toString() }
|
.map { categories[it].id.toString() }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
preferences.downloadNewCategories().set(included)
|
preferences.downloadNewChapterCategories().set(included)
|
||||||
preferences.downloadNewCategoriesExclude().set(excluded)
|
preferences.downloadNewChapterCategoriesExclude().set(excluded)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ 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.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||||
|
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
@@ -185,8 +187,8 @@ class SettingsLibraryController : SettingsController() {
|
|||||||
multiSelectListPreference {
|
multiSelectListPreference {
|
||||||
bindTo(preferences.libraryUpdateDeviceRestriction())
|
bindTo(preferences.libraryUpdateDeviceRestriction())
|
||||||
titleRes = R.string.pref_library_update_restriction
|
titleRes = R.string.pref_library_update_restriction
|
||||||
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
|
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low)
|
||||||
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_CHARGING)
|
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
|
||||||
|
|
||||||
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
||||||
|
|
||||||
@@ -202,7 +204,9 @@ class SettingsLibraryController : SettingsController() {
|
|||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
|
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
|
||||||
|
DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered)
|
||||||
DEVICE_CHARGING -> context.getString(R.string.charging)
|
DEVICE_CHARGING -> context.getString(R.string.charging)
|
||||||
|
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
|
||||||
else -> it
|
else -> it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ class ClearDatabaseController :
|
|||||||
|
|
||||||
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(activity)
|
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
|
||||||
binding.recycler.setHasFixedSize(true)
|
binding.recycler.setHasFixedSize(true)
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
recycler = binding.recycler
|
recycler = binding.recycler
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
|||||||
if (!favorite) return false
|
if (!favorite) return false
|
||||||
|
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
val downloadNew = prefs.downloadNew().get()
|
val downloadNewChapter = prefs.downloadNewChapter().get()
|
||||||
if (!downloadNew) return false
|
if (!downloadNewChapter) return false
|
||||||
|
|
||||||
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
|
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
|
||||||
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
|
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
|
||||||
|
|
||||||
// Default: download from all categories
|
// Default: Download from all categories
|
||||||
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true
|
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
|
||||||
|
|
||||||
// Get all categories, else default category (0)
|
// Get all categories, else default category (0)
|
||||||
val categoriesForManga =
|
val categoriesForManga =
|
||||||
@@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
|||||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||||
|
|
||||||
// In excluded category
|
// In excluded category
|
||||||
if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) return false
|
if (categoriesForManga.any { it in excludedCategories }) return false
|
||||||
|
|
||||||
|
// Included category not selected
|
||||||
|
if (includedCategories.isEmpty()) return true
|
||||||
|
|
||||||
// In included category
|
// In included category
|
||||||
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty()
|
return categoriesForManga.any { it in includedCategories }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
||||||
@@ -60,6 +61,9 @@ fun syncChaptersWithSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maxTimestamp = 0L // in previous chapters to add
|
||||||
|
val rightNow = Date().time
|
||||||
|
|
||||||
for (sourceChapter in sourceChapters) {
|
for (sourceChapter in sourceChapters) {
|
||||||
// This forces metadata update for the main viewable things in the chapter list.
|
// This forces metadata update for the main viewable things in the chapter list.
|
||||||
if (source is HttpSource) {
|
if (source is HttpSource) {
|
||||||
@@ -73,7 +77,9 @@ fun syncChaptersWithSource(
|
|||||||
// Add the chapter if not in db already, or update if the metadata changed.
|
// Add the chapter if not in db already, or update if the metadata changed.
|
||||||
if (dbChapter == null) {
|
if (dbChapter == null) {
|
||||||
if (sourceChapter.date_upload == 0L) {
|
if (sourceChapter.date_upload == 0L) {
|
||||||
sourceChapter.date_upload = Date().time
|
sourceChapter.date_upload = if (maxTimestamp == 0L) rightNow else maxTimestamp
|
||||||
|
} else {
|
||||||
|
maxTimestamp = max(maxTimestamp, sourceChapter.date_upload)
|
||||||
}
|
}
|
||||||
toAdd.add(sourceChapter)
|
toAdd.add(sourceChapter)
|
||||||
} else {
|
} else {
|
||||||
@@ -98,6 +104,7 @@ fun syncChaptersWithSource(
|
|||||||
return Pair(emptyList(), emptyList())
|
return Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep it a List instead of a Set. See #6372.
|
||||||
val readded = mutableListOf<Chapter>()
|
val readded = mutableListOf<Chapter>()
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
@@ -170,6 +177,7 @@ fun syncChaptersWithSource(
|
|||||||
db.updateLastUpdated(manga).executeAsBlocking()
|
db.updateLastUpdated(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ConvertArgumentToSet")
|
||||||
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
|
||||||
|
val isDevFlavor: Boolean
|
||||||
|
get() = BuildConfig.FLAVOR == "dev"
|
||||||
@@ -87,7 +87,11 @@ fun Context.copyToClipboard(label: String, content: String) {
|
|||||||
val clipboard = getSystemService<ClipboardManager>()!!
|
val clipboard = getSystemService<ClipboardManager>()!!
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
||||||
|
|
||||||
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
// Android 13 and higher shows a visual confirmation of copied contents
|
||||||
|
// https://developer.android.com/about/versions/13/features/copy-paste
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
toast(R.string.clipboard_copy_error)
|
toast(R.string.clipboard_copy_error)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.graphics.drawable.ColorDrawable
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.graphics.alpha
|
import androidx.core.graphics.alpha
|
||||||
import androidx.core.graphics.applyCanvas
|
import androidx.core.graphics.applyCanvas
|
||||||
@@ -59,6 +60,12 @@ object ImageUtil {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getExtensionFromMimeType(mime: String?): String {
|
||||||
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||||
|
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
|
||||||
|
?: "jpg"
|
||||||
|
}
|
||||||
|
|
||||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||||
try {
|
try {
|
||||||
val type = getImageType(stream) ?: return false
|
val type = getImageType(stream) ?: return false
|
||||||
@@ -396,6 +403,12 @@ object ImageUtil {
|
|||||||
private fun Int.isWhite(): Boolean =
|
private fun Int.isWhite(): Boolean =
|
||||||
red + blue + green > 740
|
red + blue + green > 740
|
||||||
|
|
||||||
|
// Android doesn't include some mappings
|
||||||
|
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||||
|
// https://issuetracker.google.com/issues/182703810
|
||||||
|
"image/jxl" to "jxl",
|
||||||
|
)
|
||||||
|
|
||||||
fun mergeBitmaps(
|
fun mergeBitmaps(
|
||||||
imageBitmap: Bitmap,
|
imageBitmap: Bitmap,
|
||||||
imageBitmap2: Bitmap,
|
imageBitmap2: Bitmap,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
|||||||
* @param context any context.
|
* @param context any context.
|
||||||
* @param resId the vector resource to load and tint
|
* @param resId the vector resource to load and tint
|
||||||
*/
|
*/
|
||||||
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
|
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
|
||||||
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
||||||
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||||||
val tint = if (state == State.UNCHECKED) {
|
val tint = if (state == State.UNCHECKED) {
|
||||||
context.getThemeColor(R.attr.colorControlNormal)
|
context.getThemeColor(R.attr.colorControlNormal)
|
||||||
} else {
|
} else {
|
||||||
context.getThemeColor(R.attr.colorAccent)
|
context.getThemeColor(R.attr.colorPrimary)
|
||||||
}
|
}
|
||||||
if (tint != 0) {
|
if (tint != 0) {
|
||||||
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package exh
|
package exh
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -309,7 +311,6 @@ object EXHMigrations {
|
|||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
val newSortingMode = when (oldSortingMode) {
|
||||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||||
|
|||||||
+26
-24406
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Cosplayer : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"cosplayer:akane araragi",
|
||||||
|
"cosplayer:aleksandra bodler",
|
||||||
|
"cosplayer:aokotan",
|
||||||
|
"cosplayer:arty huang",
|
||||||
|
"cosplayer:atsuki",
|
||||||
|
"cosplayer:bishoujomom",
|
||||||
|
"cosplayer:carry key",
|
||||||
|
"cosplayer:chunmomo",
|
||||||
|
"cosplayer:gumiho hannya",
|
||||||
|
"cosplayer:hane ame",
|
||||||
|
"cosplayer:hinaughtya",
|
||||||
|
"cosplayer:holly wolf",
|
||||||
|
"cosplayer:iori moe",
|
||||||
|
"cosplayer:jill",
|
||||||
|
"cosplayer:kalinka fox",
|
||||||
|
"cosplayer:kaya huang",
|
||||||
|
"cosplayer:kuuko w",
|
||||||
|
"cosplayer:lenfried",
|
||||||
|
"cosplayer:miih cosplay",
|
||||||
|
"cosplayer:mikomin",
|
||||||
|
"cosplayer:misa daidai",
|
||||||
|
"cosplayer:momoiro reku",
|
||||||
|
"cosplayer:momokun",
|
||||||
|
"cosplayer:nadyasonika",
|
||||||
|
"cosplayer:nora fawn",
|
||||||
|
"cosplayer:octokuro",
|
||||||
|
"cosplayer:oichi",
|
||||||
|
"cosplayer:rioko",
|
||||||
|
"cosplayer:rocksy light",
|
||||||
|
"cosplayer:saku",
|
||||||
|
"cosplayer:sakurai hinoki",
|
||||||
|
"cosplayer:shiro kitsune",
|
||||||
|
"cosplayer:siao ding",
|
||||||
|
"cosplayer:smoettii",
|
||||||
|
"cosplayer:valery himera",
|
||||||
|
"cosplayer:velvet",
|
||||||
|
"cosplayer:wildhoney423",
|
||||||
|
"cosplayer:yume",
|
||||||
|
"cosplayer:yuzupyon",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Female : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"female:abortion",
|
||||||
|
"female:absorption",
|
||||||
|
"female:adventitious vagina",
|
||||||
|
"female:age progression",
|
||||||
|
"female:age regression",
|
||||||
|
"female:ahegao",
|
||||||
|
"female:albino",
|
||||||
|
"female:alien girl",
|
||||||
|
"female:all the way through",
|
||||||
|
"female:amputee",
|
||||||
|
"female:anal",
|
||||||
|
"female:anal birth",
|
||||||
|
"female:anal intercourse",
|
||||||
|
"female:anal prolapse",
|
||||||
|
"female:angel",
|
||||||
|
"female:animal on furry",
|
||||||
|
"female:animegao",
|
||||||
|
"female:anorexic",
|
||||||
|
"female:apron",
|
||||||
|
"female:armpit licking",
|
||||||
|
"female:armpit sex",
|
||||||
|
"female:asphyxiation",
|
||||||
|
"female:ass expansion",
|
||||||
|
"female:assjob",
|
||||||
|
"female:aunt",
|
||||||
|
"female:autofellatio",
|
||||||
|
"female:autopaizuri",
|
||||||
|
"female:bald",
|
||||||
|
"female:ball sucking",
|
||||||
|
"female:balljob",
|
||||||
|
"female:bandages",
|
||||||
|
"female:bandaid",
|
||||||
|
"female:bbw",
|
||||||
|
"female:bdsm",
|
||||||
|
"female:bear",
|
||||||
|
"female:bear girl",
|
||||||
|
"female:beauty mark",
|
||||||
|
"female:bee girl",
|
||||||
|
"female:bestiality",
|
||||||
|
"female:big areolae",
|
||||||
|
"female:big ass",
|
||||||
|
"female:big balls",
|
||||||
|
"female:big breasts",
|
||||||
|
"female:big clit",
|
||||||
|
"female:big nipples",
|
||||||
|
"female:big penis",
|
||||||
|
"female:big vagina",
|
||||||
|
"female:bike shorts",
|
||||||
|
"female:bikini",
|
||||||
|
"female:birth",
|
||||||
|
"female:bisexual",
|
||||||
|
"female:blackmail",
|
||||||
|
"female:blind",
|
||||||
|
"female:blindfold",
|
||||||
|
"female:blood",
|
||||||
|
"female:bloomers",
|
||||||
|
"female:blowjob",
|
||||||
|
"female:blowjob face",
|
||||||
|
"female:body modification",
|
||||||
|
"female:body painting",
|
||||||
|
"female:body swap",
|
||||||
|
"female:body writing",
|
||||||
|
"female:bodystocking",
|
||||||
|
"female:bodysuit",
|
||||||
|
"female:bondage",
|
||||||
|
"female:brain fuck",
|
||||||
|
"female:breast expansion",
|
||||||
|
"female:breast feeding",
|
||||||
|
"female:breast reduction",
|
||||||
|
"female:bride",
|
||||||
|
"female:bukkake",
|
||||||
|
"female:bunny girl",
|
||||||
|
"female:burping",
|
||||||
|
"female:business suit",
|
||||||
|
"female:butler",
|
||||||
|
"female:cannibalism",
|
||||||
|
"female:cashier",
|
||||||
|
"female:catfight",
|
||||||
|
"female:catgirl",
|
||||||
|
"female:cbt",
|
||||||
|
"female:centaur",
|
||||||
|
"female:cervix penetration",
|
||||||
|
"female:cervix prolapse",
|
||||||
|
"female:chastity belt",
|
||||||
|
"female:cheating",
|
||||||
|
"female:cheerleader",
|
||||||
|
"female:chikan",
|
||||||
|
"female:chinese dress",
|
||||||
|
"female:chloroform",
|
||||||
|
"female:christmas",
|
||||||
|
"female:clamp",
|
||||||
|
"female:clit growth",
|
||||||
|
"female:clit stimulation",
|
||||||
|
"female:clone",
|
||||||
|
"female:clothed male nude female",
|
||||||
|
"female:coach",
|
||||||
|
"female:cock ring",
|
||||||
|
"female:cockslapping",
|
||||||
|
"female:collar",
|
||||||
|
"female:condom",
|
||||||
|
"female:conjoined",
|
||||||
|
"female:coprophagia",
|
||||||
|
"female:corruption",
|
||||||
|
"female:corset",
|
||||||
|
"female:cosplaying",
|
||||||
|
"female:cousin",
|
||||||
|
"female:cow",
|
||||||
|
"female:cowgirl",
|
||||||
|
"female:crab",
|
||||||
|
"female:crossdressing",
|
||||||
|
"female:crotch tattoo",
|
||||||
|
"female:crown",
|
||||||
|
"female:cum bath",
|
||||||
|
"female:cum in eye",
|
||||||
|
"female:cum swap",
|
||||||
|
"female:cumflation",
|
||||||
|
"female:cunnilingus",
|
||||||
|
"female:dark nipples",
|
||||||
|
"female:dark sclera",
|
||||||
|
"female:dark skin",
|
||||||
|
"female:daughter",
|
||||||
|
"female:deepthroat",
|
||||||
|
"female:deer",
|
||||||
|
"female:deer girl",
|
||||||
|
"female:defloration",
|
||||||
|
"female:demon girl",
|
||||||
|
"female:diaper",
|
||||||
|
"female:dick growth",
|
||||||
|
"female:dickgirl on dickgirl",
|
||||||
|
"female:dickgirls only",
|
||||||
|
"female:dicknipples",
|
||||||
|
"female:dinosaur",
|
||||||
|
"female:dog",
|
||||||
|
"female:dog girl",
|
||||||
|
"female:doll joints",
|
||||||
|
"female:donkey",
|
||||||
|
"female:double anal",
|
||||||
|
"female:double blowjob",
|
||||||
|
"female:double penetration",
|
||||||
|
"female:double vaginal",
|
||||||
|
"female:dougi",
|
||||||
|
"female:draenei",
|
||||||
|
"female:dragon",
|
||||||
|
"female:drill hair",
|
||||||
|
"female:drugs",
|
||||||
|
"female:drunk",
|
||||||
|
"female:ear fuck",
|
||||||
|
"female:eel",
|
||||||
|
"female:eggs",
|
||||||
|
"female:electric shocks",
|
||||||
|
"female:elephant",
|
||||||
|
"female:elf",
|
||||||
|
"female:emotionless sex",
|
||||||
|
"female:enema",
|
||||||
|
"female:exhibitionism",
|
||||||
|
"female:exposed clothing",
|
||||||
|
"female:eye penetration",
|
||||||
|
"female:eye-covering bang",
|
||||||
|
"female:eyemask",
|
||||||
|
"female:eyepatch",
|
||||||
|
"female:facesitting",
|
||||||
|
"female:facial hair",
|
||||||
|
"female:fairy",
|
||||||
|
"female:farting",
|
||||||
|
"female:females only",
|
||||||
|
"female:femdom",
|
||||||
|
"female:fft threesome",
|
||||||
|
"female:filming",
|
||||||
|
"female:fingering",
|
||||||
|
"female:first person perspective",
|
||||||
|
"female:fish",
|
||||||
|
"female:fishnets",
|
||||||
|
"female:fisting",
|
||||||
|
"female:focus anal",
|
||||||
|
"female:focus blowjob",
|
||||||
|
"female:focus paizuri",
|
||||||
|
"female:food on body",
|
||||||
|
"female:foot insertion",
|
||||||
|
"female:foot licking",
|
||||||
|
"female:footjob",
|
||||||
|
"female:forniphilia",
|
||||||
|
"female:fox",
|
||||||
|
"female:fox girl",
|
||||||
|
"female:freckles",
|
||||||
|
"female:frog",
|
||||||
|
"female:frottage",
|
||||||
|
"female:fundoshi",
|
||||||
|
"female:furry",
|
||||||
|
"female:futanari",
|
||||||
|
"female:gag",
|
||||||
|
"female:gaping",
|
||||||
|
"female:garter belt",
|
||||||
|
"female:gasmask",
|
||||||
|
"female:gender change",
|
||||||
|
"female:gender morph",
|
||||||
|
"female:ghost",
|
||||||
|
"female:giantess",
|
||||||
|
"female:gigantic breasts",
|
||||||
|
"female:glasses",
|
||||||
|
"female:glory hole",
|
||||||
|
"female:gloves",
|
||||||
|
"female:gokkun",
|
||||||
|
"female:gothic lolita",
|
||||||
|
"female:granddaughter",
|
||||||
|
"female:grandmother",
|
||||||
|
"female:group",
|
||||||
|
"female:growth",
|
||||||
|
"female:guro",
|
||||||
|
"female:gyaru",
|
||||||
|
"female:gymshorts",
|
||||||
|
"female:hair buns",
|
||||||
|
"female:hairjob",
|
||||||
|
"female:hairy",
|
||||||
|
"female:hairy armpits",
|
||||||
|
"female:handicapped",
|
||||||
|
"female:handjob",
|
||||||
|
"female:harem",
|
||||||
|
"female:harness",
|
||||||
|
"female:harpy",
|
||||||
|
"female:headless",
|
||||||
|
"female:headphones",
|
||||||
|
"female:heterochromia",
|
||||||
|
"female:hijab",
|
||||||
|
"female:hood",
|
||||||
|
"female:horns",
|
||||||
|
"female:horse",
|
||||||
|
"female:horse cock",
|
||||||
|
"female:horse girl",
|
||||||
|
"female:hotpants",
|
||||||
|
"female:huge breasts",
|
||||||
|
"female:huge penis",
|
||||||
|
"female:human cattle",
|
||||||
|
"female:human on furry",
|
||||||
|
"female:humiliation",
|
||||||
|
"female:impregnation",
|
||||||
|
"female:incest",
|
||||||
|
"female:infantilism",
|
||||||
|
"female:inflation",
|
||||||
|
"female:insect",
|
||||||
|
"female:insect girl",
|
||||||
|
"female:inverted nipples",
|
||||||
|
"female:invisible",
|
||||||
|
"female:kemonomimi",
|
||||||
|
"female:kigurumi pajama",
|
||||||
|
"female:kimono",
|
||||||
|
"female:kissing",
|
||||||
|
"female:kunoichi",
|
||||||
|
"female:lab coat",
|
||||||
|
"female:lactation",
|
||||||
|
"female:large insertions",
|
||||||
|
"female:large tattoo",
|
||||||
|
"female:latex",
|
||||||
|
"female:layer cake",
|
||||||
|
"female:leash",
|
||||||
|
"female:leg lock",
|
||||||
|
"female:leotard",
|
||||||
|
"female:lingerie",
|
||||||
|
"female:lioness",
|
||||||
|
"female:living clothes",
|
||||||
|
"female:lizard girl",
|
||||||
|
"female:lolicon",
|
||||||
|
"female:long tongue",
|
||||||
|
"female:low bestiality",
|
||||||
|
"female:low lolicon",
|
||||||
|
"female:machine",
|
||||||
|
"female:maggot",
|
||||||
|
"female:magical girl",
|
||||||
|
"female:maid",
|
||||||
|
"female:makeup",
|
||||||
|
"female:male on dickgirl",
|
||||||
|
"female:masked face",
|
||||||
|
"female:masturbation",
|
||||||
|
"female:mecha girl",
|
||||||
|
"female:menstruation",
|
||||||
|
"female:mermaid",
|
||||||
|
"female:mesuiki",
|
||||||
|
"female:metal armor",
|
||||||
|
"female:midget",
|
||||||
|
"female:miko",
|
||||||
|
"female:milf",
|
||||||
|
"female:military",
|
||||||
|
"female:milking",
|
||||||
|
"female:mind break",
|
||||||
|
"female:mind control",
|
||||||
|
"female:minigirl",
|
||||||
|
"female:monkey",
|
||||||
|
"female:monkey girl",
|
||||||
|
"female:monster girl",
|
||||||
|
"female:moral degeneration",
|
||||||
|
"female:mother",
|
||||||
|
"female:mouse",
|
||||||
|
"female:mouse girl",
|
||||||
|
"female:mouth mask",
|
||||||
|
"female:multiple arms",
|
||||||
|
"female:multiple assjob",
|
||||||
|
"female:multiple breasts",
|
||||||
|
"female:multiple footjob",
|
||||||
|
"female:multiple handjob",
|
||||||
|
"female:multiple orgasms",
|
||||||
|
"female:multiple paizuri",
|
||||||
|
"female:multiple penises",
|
||||||
|
"female:muscle",
|
||||||
|
"female:muscle growth",
|
||||||
|
"female:nakadashi",
|
||||||
|
"female:navel fuck",
|
||||||
|
"female:nazi",
|
||||||
|
"female:necrophilia",
|
||||||
|
"female:netorare",
|
||||||
|
"female:niece",
|
||||||
|
"female:nipple birth",
|
||||||
|
"female:nipple expansion",
|
||||||
|
"female:nipple fuck",
|
||||||
|
"female:nose fuck",
|
||||||
|
"female:nose hook",
|
||||||
|
"female:nun",
|
||||||
|
"female:nurse",
|
||||||
|
"female:octopus",
|
||||||
|
"female:oil",
|
||||||
|
"female:old lady",
|
||||||
|
"female:onahole",
|
||||||
|
"female:oni",
|
||||||
|
"female:oppai loli",
|
||||||
|
"female:orc",
|
||||||
|
"female:orgasm denial",
|
||||||
|
"female:paizuri",
|
||||||
|
"female:pantyhose",
|
||||||
|
"female:pantyjob",
|
||||||
|
"female:parasite",
|
||||||
|
"female:pasties",
|
||||||
|
"female:penis birth",
|
||||||
|
"female:petplay",
|
||||||
|
"female:petrification",
|
||||||
|
"female:phimosis",
|
||||||
|
"female:phone sex",
|
||||||
|
"female:piercing",
|
||||||
|
"female:pig",
|
||||||
|
"female:pig girl",
|
||||||
|
"female:pillory",
|
||||||
|
"female:pirate",
|
||||||
|
"female:piss drinking",
|
||||||
|
"female:pixie cut",
|
||||||
|
"female:plant girl",
|
||||||
|
"female:pole dancing",
|
||||||
|
"female:policewoman",
|
||||||
|
"female:ponygirl",
|
||||||
|
"female:ponytail",
|
||||||
|
"female:possession",
|
||||||
|
"female:pregnant",
|
||||||
|
"female:prehensile hair",
|
||||||
|
"female:prolapse",
|
||||||
|
"female:prostate massage",
|
||||||
|
"female:prostitution",
|
||||||
|
"female:pubic stubble",
|
||||||
|
"female:public use",
|
||||||
|
"female:rabbit",
|
||||||
|
"female:raccoon girl",
|
||||||
|
"female:race queen",
|
||||||
|
"female:randoseru",
|
||||||
|
"female:rape",
|
||||||
|
"female:real doll",
|
||||||
|
"female:reptile",
|
||||||
|
"female:rhinoceros",
|
||||||
|
"female:rimjob",
|
||||||
|
"female:robot",
|
||||||
|
"female:ryona",
|
||||||
|
"female:saliva",
|
||||||
|
"female:scar",
|
||||||
|
"female:scat",
|
||||||
|
"female:school gym uniform",
|
||||||
|
"female:school swimsuit",
|
||||||
|
"female:schoolboy uniform",
|
||||||
|
"female:schoolgirl uniform",
|
||||||
|
"female:scrotal lingerie",
|
||||||
|
"female:selfcest",
|
||||||
|
"female:sex toys",
|
||||||
|
"female:shared senses",
|
||||||
|
"female:shark",
|
||||||
|
"female:sheep",
|
||||||
|
"female:sheep girl",
|
||||||
|
"female:shemale",
|
||||||
|
"female:shibari",
|
||||||
|
"female:shimapan",
|
||||||
|
"female:shrinking",
|
||||||
|
"female:sister",
|
||||||
|
"female:skinsuit",
|
||||||
|
"female:slave",
|
||||||
|
"female:sleeping",
|
||||||
|
"female:slime",
|
||||||
|
"female:slime girl",
|
||||||
|
"female:slug",
|
||||||
|
"female:small breasts",
|
||||||
|
"female:smegma",
|
||||||
|
"female:smell",
|
||||||
|
"female:smoking",
|
||||||
|
"female:snail girl",
|
||||||
|
"female:snake",
|
||||||
|
"female:snake girl",
|
||||||
|
"female:snuff",
|
||||||
|
"female:sole dickgirl",
|
||||||
|
"female:sole female",
|
||||||
|
"female:solo action",
|
||||||
|
"female:spanking",
|
||||||
|
"female:speculum",
|
||||||
|
"female:spider",
|
||||||
|
"female:spider girl",
|
||||||
|
"female:squid girl",
|
||||||
|
"female:squirting",
|
||||||
|
"female:ssbbw",
|
||||||
|
"female:stewardess",
|
||||||
|
"female:stockings",
|
||||||
|
"female:stomach deformation",
|
||||||
|
"female:strap-on",
|
||||||
|
"female:stretching",
|
||||||
|
"female:stuck in wall",
|
||||||
|
"female:sumata",
|
||||||
|
"female:sundress",
|
||||||
|
"female:sunglasses",
|
||||||
|
"female:sweating",
|
||||||
|
"female:swimsuit",
|
||||||
|
"female:swinging",
|
||||||
|
"female:syringe",
|
||||||
|
"female:table masturbation",
|
||||||
|
"female:tail",
|
||||||
|
"female:tail plug",
|
||||||
|
"female:tall girl",
|
||||||
|
"female:tanlines",
|
||||||
|
"female:teacher",
|
||||||
|
"female:tentacles",
|
||||||
|
"female:thigh high boots",
|
||||||
|
"female:tiara",
|
||||||
|
"female:tickling",
|
||||||
|
"female:tiger",
|
||||||
|
"female:tights",
|
||||||
|
"female:toddlercon",
|
||||||
|
"female:tomboy",
|
||||||
|
"female:tooth brushing",
|
||||||
|
"female:torture",
|
||||||
|
"female:tracksuit",
|
||||||
|
"female:trampling",
|
||||||
|
"female:transformation",
|
||||||
|
"female:tribadism",
|
||||||
|
"female:triple anal",
|
||||||
|
"female:triple penetration",
|
||||||
|
"female:triple vaginal",
|
||||||
|
"female:ttf threesome",
|
||||||
|
"female:tube",
|
||||||
|
"female:turtle",
|
||||||
|
"female:tutor",
|
||||||
|
"female:twins",
|
||||||
|
"female:twintails",
|
||||||
|
"female:unbirth",
|
||||||
|
"female:underwater",
|
||||||
|
"female:unicorn",
|
||||||
|
"female:unusual insertions",
|
||||||
|
"female:unusual pupils",
|
||||||
|
"female:unusual teeth",
|
||||||
|
"female:urethra insertion",
|
||||||
|
"female:urination",
|
||||||
|
"female:vacbed",
|
||||||
|
"female:vaginal sticker",
|
||||||
|
"female:vampire",
|
||||||
|
"female:very long hair",
|
||||||
|
"female:vomit",
|
||||||
|
"female:vore",
|
||||||
|
"female:voyeurism",
|
||||||
|
"female:vtuber",
|
||||||
|
"female:waiter",
|
||||||
|
"female:waitress",
|
||||||
|
"female:weight gain",
|
||||||
|
"female:whip",
|
||||||
|
"female:wings",
|
||||||
|
"female:witch",
|
||||||
|
"female:wolf",
|
||||||
|
"female:wolf girl",
|
||||||
|
"female:wooden horse",
|
||||||
|
"female:worm",
|
||||||
|
"female:wormhole",
|
||||||
|
"female:wrestling",
|
||||||
|
"female:x-ray",
|
||||||
|
"female:yandere",
|
||||||
|
"female:yuri",
|
||||||
|
"female:zombie",
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Language : TagList {
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"language:arabic",
|
||||||
|
"language:bulgarian",
|
||||||
|
"language:catalan",
|
||||||
|
"language:cebuano",
|
||||||
|
"language:chinese",
|
||||||
|
"language:cree",
|
||||||
|
"language:creole",
|
||||||
|
"language:czech",
|
||||||
|
"language:danish",
|
||||||
|
"language:dutch",
|
||||||
|
"language:english",
|
||||||
|
"language:finnish",
|
||||||
|
"language:french",
|
||||||
|
"language:german",
|
||||||
|
"language:greek",
|
||||||
|
"language:hindi",
|
||||||
|
"language:hungarian",
|
||||||
|
"language:indonesian",
|
||||||
|
"language:irish",
|
||||||
|
"language:italian",
|
||||||
|
"language:japanese",
|
||||||
|
"language:korean",
|
||||||
|
"language:ladino",
|
||||||
|
"language:lao",
|
||||||
|
"language:norwegian",
|
||||||
|
"language:persian",
|
||||||
|
"language:polish",
|
||||||
|
"language:portuguese",
|
||||||
|
"language:rewrite",
|
||||||
|
"language:romanian",
|
||||||
|
"language:russian",
|
||||||
|
"language:sango",
|
||||||
|
"language:spanish",
|
||||||
|
"language:speechless",
|
||||||
|
"language:swedish",
|
||||||
|
"language:tagalog",
|
||||||
|
"language:text cleaned",
|
||||||
|
"language:thai",
|
||||||
|
"language:tigrinya",
|
||||||
|
"language:translated",
|
||||||
|
"language:turkish",
|
||||||
|
"language:vietnamese",
|
||||||
|
"language:zulu",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Male : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"male:abortion",
|
||||||
|
"male:absorption",
|
||||||
|
"male:age progression",
|
||||||
|
"male:age regression",
|
||||||
|
"male:ahegao",
|
||||||
|
"male:alien",
|
||||||
|
"male:amputee",
|
||||||
|
"male:anal",
|
||||||
|
"male:anal birth",
|
||||||
|
"male:anal intercourse",
|
||||||
|
"male:anal prolapse",
|
||||||
|
"male:angel",
|
||||||
|
"male:animal on furry",
|
||||||
|
"male:animegao",
|
||||||
|
"male:apparel bukkake",
|
||||||
|
"male:apron",
|
||||||
|
"male:armpit licking",
|
||||||
|
"male:asphyxiation",
|
||||||
|
"male:bald",
|
||||||
|
"male:ball sucking",
|
||||||
|
"male:balls expansion",
|
||||||
|
"male:bandages",
|
||||||
|
"male:bat boy",
|
||||||
|
"male:bbm",
|
||||||
|
"male:bdsm",
|
||||||
|
"male:bear",
|
||||||
|
"male:bear boy",
|
||||||
|
"male:bestiality",
|
||||||
|
"male:big ass",
|
||||||
|
"male:big balls",
|
||||||
|
"male:big breasts",
|
||||||
|
"male:big nipples",
|
||||||
|
"male:big penis",
|
||||||
|
"male:bike shorts",
|
||||||
|
"male:bikini",
|
||||||
|
"male:birth",
|
||||||
|
"male:bisexual",
|
||||||
|
"male:blackmail",
|
||||||
|
"male:blind",
|
||||||
|
"male:blindfold",
|
||||||
|
"male:blood",
|
||||||
|
"male:bloomers",
|
||||||
|
"male:blowjob",
|
||||||
|
"male:blowjob face",
|
||||||
|
"male:body painting",
|
||||||
|
"male:body swap",
|
||||||
|
"male:body writing",
|
||||||
|
"male:bodystocking",
|
||||||
|
"male:bodysuit",
|
||||||
|
"male:bondage",
|
||||||
|
"male:breast feeding",
|
||||||
|
"male:bride",
|
||||||
|
"male:brother",
|
||||||
|
"male:bukkake",
|
||||||
|
"male:bull",
|
||||||
|
"male:bunny boy",
|
||||||
|
"male:burping",
|
||||||
|
"male:business suit",
|
||||||
|
"male:butler",
|
||||||
|
"male:cannibalism",
|
||||||
|
"male:cashier",
|
||||||
|
"male:cat",
|
||||||
|
"male:catboy",
|
||||||
|
"male:cbt",
|
||||||
|
"male:cervix prolapse",
|
||||||
|
"male:chastity belt",
|
||||||
|
"male:cheating",
|
||||||
|
"male:cheerleader",
|
||||||
|
"male:chikan",
|
||||||
|
"male:chinese dress",
|
||||||
|
"male:chloroform",
|
||||||
|
"male:christmas",
|
||||||
|
"male:clit stimulation",
|
||||||
|
"male:clone",
|
||||||
|
"male:clothed female nude male",
|
||||||
|
"male:coach",
|
||||||
|
"male:cock ring",
|
||||||
|
"male:collar",
|
||||||
|
"male:condom",
|
||||||
|
"male:coprophagia",
|
||||||
|
"male:corruption",
|
||||||
|
"male:corset",
|
||||||
|
"male:cosplaying",
|
||||||
|
"male:cousin",
|
||||||
|
"male:cowman",
|
||||||
|
"male:crab",
|
||||||
|
"male:crossdressing",
|
||||||
|
"male:crown",
|
||||||
|
"male:cuntboy",
|
||||||
|
"male:dark nipples",
|
||||||
|
"male:dark sclera",
|
||||||
|
"male:dark skin",
|
||||||
|
"male:deepthroat",
|
||||||
|
"male:deer",
|
||||||
|
"male:demon",
|
||||||
|
"male:dick growth",
|
||||||
|
"male:dickgirl on male",
|
||||||
|
"male:dilf",
|
||||||
|
"male:dinosaur",
|
||||||
|
"male:dog",
|
||||||
|
"male:dog boy",
|
||||||
|
"male:donkey",
|
||||||
|
"male:double anal",
|
||||||
|
"male:double blowjob",
|
||||||
|
"male:double penetration",
|
||||||
|
"male:dougi",
|
||||||
|
"male:dragon",
|
||||||
|
"male:drill hair",
|
||||||
|
"male:drugs",
|
||||||
|
"male:drunk",
|
||||||
|
"male:eel",
|
||||||
|
"male:eggs",
|
||||||
|
"male:electric shocks",
|
||||||
|
"male:elephant",
|
||||||
|
"male:elf",
|
||||||
|
"male:emotionless sex",
|
||||||
|
"male:enema",
|
||||||
|
"male:exhibitionism",
|
||||||
|
"male:exposed clothing",
|
||||||
|
"male:eye penetration",
|
||||||
|
"male:eye-covering bang",
|
||||||
|
"male:eyemask",
|
||||||
|
"male:eyepatch",
|
||||||
|
"male:facial hair",
|
||||||
|
"male:fairy",
|
||||||
|
"male:farting",
|
||||||
|
"male:father",
|
||||||
|
"male:feminization",
|
||||||
|
"male:filming",
|
||||||
|
"male:first person perspective",
|
||||||
|
"male:fish",
|
||||||
|
"male:fishnets",
|
||||||
|
"male:fisting",
|
||||||
|
"male:focus paizuri",
|
||||||
|
"male:food on body",
|
||||||
|
"male:foot licking",
|
||||||
|
"male:footjob",
|
||||||
|
"male:forniphilia",
|
||||||
|
"male:fox",
|
||||||
|
"male:fox boy",
|
||||||
|
"male:freckles",
|
||||||
|
"male:frog",
|
||||||
|
"male:frottage",
|
||||||
|
"male:fundoshi",
|
||||||
|
"male:furry",
|
||||||
|
"male:gag",
|
||||||
|
"male:gaping",
|
||||||
|
"male:garter belt",
|
||||||
|
"male:gasmask",
|
||||||
|
"male:gender change",
|
||||||
|
"male:gender morph",
|
||||||
|
"male:ghost",
|
||||||
|
"male:giant",
|
||||||
|
"male:glasses",
|
||||||
|
"male:glory hole",
|
||||||
|
"male:goblin",
|
||||||
|
"male:gokkun",
|
||||||
|
"male:gorilla",
|
||||||
|
"male:gothic lolita",
|
||||||
|
"male:grandfather",
|
||||||
|
"male:group",
|
||||||
|
"male:growth",
|
||||||
|
"male:guro",
|
||||||
|
"male:gyaru-oh",
|
||||||
|
"male:gymshorts",
|
||||||
|
"male:hair buns",
|
||||||
|
"male:hairy",
|
||||||
|
"male:handjob",
|
||||||
|
"male:harem",
|
||||||
|
"male:harpy",
|
||||||
|
"male:headphones",
|
||||||
|
"male:horns",
|
||||||
|
"male:horse",
|
||||||
|
"male:horse boy",
|
||||||
|
"male:horse cock",
|
||||||
|
"male:hotpants",
|
||||||
|
"male:human on furry",
|
||||||
|
"male:humiliation",
|
||||||
|
"male:impregnation",
|
||||||
|
"male:incest",
|
||||||
|
"male:infantilism",
|
||||||
|
"male:inflation",
|
||||||
|
"male:insect",
|
||||||
|
"male:insect boy",
|
||||||
|
"male:invisible",
|
||||||
|
"male:josou seme",
|
||||||
|
"male:kemonomimi",
|
||||||
|
"male:kigurumi pajama",
|
||||||
|
"male:kimono",
|
||||||
|
"male:kissing",
|
||||||
|
"male:lab coat",
|
||||||
|
"male:large insertions",
|
||||||
|
"male:large tattoo",
|
||||||
|
"male:latex",
|
||||||
|
"male:layer cake",
|
||||||
|
"male:leotard",
|
||||||
|
"male:lingerie",
|
||||||
|
"male:lion",
|
||||||
|
"male:lizard guy",
|
||||||
|
"male:long tongue",
|
||||||
|
"male:low bestiality",
|
||||||
|
"male:low shotacon",
|
||||||
|
"male:machine",
|
||||||
|
"male:maggot",
|
||||||
|
"male:magical girl",
|
||||||
|
"male:maid",
|
||||||
|
"male:makeup",
|
||||||
|
"male:males only",
|
||||||
|
"male:masked face",
|
||||||
|
"male:masturbation",
|
||||||
|
"male:merman",
|
||||||
|
"male:mesuiki",
|
||||||
|
"male:metal armor",
|
||||||
|
"male:midget",
|
||||||
|
"male:miko",
|
||||||
|
"male:military",
|
||||||
|
"male:mind break",
|
||||||
|
"male:mind control",
|
||||||
|
"male:miniguy",
|
||||||
|
"male:minotaur",
|
||||||
|
"male:monkey",
|
||||||
|
"male:monkey boy",
|
||||||
|
"male:monster",
|
||||||
|
"male:moral degeneration",
|
||||||
|
"male:mouse",
|
||||||
|
"male:mouse boy",
|
||||||
|
"male:multiple assjob",
|
||||||
|
"male:multiple footjob",
|
||||||
|
"male:multiple handjob",
|
||||||
|
"male:multiple orgasms",
|
||||||
|
"male:multiple penises",
|
||||||
|
"male:muscle",
|
||||||
|
"male:nakadashi",
|
||||||
|
"male:necrophilia",
|
||||||
|
"male:netorare",
|
||||||
|
"male:ninja",
|
||||||
|
"male:nipple birth",
|
||||||
|
"male:nose fuck",
|
||||||
|
"male:nose hook",
|
||||||
|
"male:nun",
|
||||||
|
"male:nurse",
|
||||||
|
"male:octopus",
|
||||||
|
"male:oil",
|
||||||
|
"male:old man",
|
||||||
|
"male:onahole",
|
||||||
|
"male:orc",
|
||||||
|
"male:orgasm denial",
|
||||||
|
"male:otokofutanari",
|
||||||
|
"male:paizuri",
|
||||||
|
"male:panther",
|
||||||
|
"male:pantyhose",
|
||||||
|
"male:pasties",
|
||||||
|
"male:pegging",
|
||||||
|
"male:penis birth",
|
||||||
|
"male:petplay",
|
||||||
|
"male:phimosis",
|
||||||
|
"male:piercing",
|
||||||
|
"male:pig",
|
||||||
|
"male:pig man",
|
||||||
|
"male:pillory",
|
||||||
|
"male:piss drinking",
|
||||||
|
"male:plant boy",
|
||||||
|
"male:pole dancing",
|
||||||
|
"male:policeman",
|
||||||
|
"male:possession",
|
||||||
|
"male:pregnant",
|
||||||
|
"male:priest",
|
||||||
|
"male:prolapse",
|
||||||
|
"male:prostate massage",
|
||||||
|
"male:prostitution",
|
||||||
|
"male:pubic stubble",
|
||||||
|
"male:public use",
|
||||||
|
"male:rabbit",
|
||||||
|
"male:randoseru",
|
||||||
|
"male:rape",
|
||||||
|
"male:reptile",
|
||||||
|
"male:rhinoceros",
|
||||||
|
"male:rimjob",
|
||||||
|
"male:robot",
|
||||||
|
"male:ryona",
|
||||||
|
"male:scar",
|
||||||
|
"male:scat",
|
||||||
|
"male:school gym uniform",
|
||||||
|
"male:school swimsuit",
|
||||||
|
"male:schoolboy uniform",
|
||||||
|
"male:schoolgirl uniform",
|
||||||
|
"male:selfcest",
|
||||||
|
"male:sex toys",
|
||||||
|
"male:shared senses",
|
||||||
|
"male:shark",
|
||||||
|
"male:shark boy",
|
||||||
|
"male:sheep",
|
||||||
|
"male:sheep boy",
|
||||||
|
"male:shibari",
|
||||||
|
"male:shimapan",
|
||||||
|
"male:shotacon",
|
||||||
|
"male:shrinking",
|
||||||
|
"male:skinsuit",
|
||||||
|
"male:slave",
|
||||||
|
"male:sleeping",
|
||||||
|
"male:slime",
|
||||||
|
"male:slime boy",
|
||||||
|
"male:slug",
|
||||||
|
"male:small penis",
|
||||||
|
"male:smegma",
|
||||||
|
"male:smell",
|
||||||
|
"male:smoking",
|
||||||
|
"male:snake",
|
||||||
|
"male:snake boy",
|
||||||
|
"male:snuff",
|
||||||
|
"male:sole male",
|
||||||
|
"male:solo action",
|
||||||
|
"male:spanking",
|
||||||
|
"male:speculum",
|
||||||
|
"male:spider",
|
||||||
|
"male:squid boy",
|
||||||
|
"male:stewardess",
|
||||||
|
"male:stockings",
|
||||||
|
"male:stomach deformation",
|
||||||
|
"male:stretching",
|
||||||
|
"male:stuck in wall",
|
||||||
|
"male:sundress",
|
||||||
|
"male:sunglasses",
|
||||||
|
"male:sweating",
|
||||||
|
"male:swimsuit",
|
||||||
|
"male:swinging",
|
||||||
|
"male:syringe",
|
||||||
|
"male:tail",
|
||||||
|
"male:tail plug",
|
||||||
|
"male:tall man",
|
||||||
|
"male:tanlines",
|
||||||
|
"male:teacher",
|
||||||
|
"male:tentacles",
|
||||||
|
"male:thigh high boots",
|
||||||
|
"male:tiara",
|
||||||
|
"male:tickling",
|
||||||
|
"male:tiger",
|
||||||
|
"male:tights",
|
||||||
|
"male:toddlercon",
|
||||||
|
"male:tomgirl",
|
||||||
|
"male:tooth brushing",
|
||||||
|
"male:torture",
|
||||||
|
"male:tracksuit",
|
||||||
|
"male:trampling",
|
||||||
|
"male:transformation",
|
||||||
|
"male:tube",
|
||||||
|
"male:turtle",
|
||||||
|
"male:tutor",
|
||||||
|
"male:twins",
|
||||||
|
"male:unbirth",
|
||||||
|
"male:uncle",
|
||||||
|
"male:unicorn",
|
||||||
|
"male:unusual pupils",
|
||||||
|
"male:urethra insertion",
|
||||||
|
"male:urination",
|
||||||
|
"male:vampire",
|
||||||
|
"male:very long hair",
|
||||||
|
"male:virginity",
|
||||||
|
"male:vomit",
|
||||||
|
"male:vore",
|
||||||
|
"male:voyeurism",
|
||||||
|
"male:waiter",
|
||||||
|
"male:waitress",
|
||||||
|
"male:whip",
|
||||||
|
"male:wings",
|
||||||
|
"male:witch",
|
||||||
|
"male:wolf",
|
||||||
|
"male:wolf boy",
|
||||||
|
"male:wooden horse",
|
||||||
|
"male:worm",
|
||||||
|
"male:wormhole",
|
||||||
|
"male:x-ray",
|
||||||
|
"male:yandere",
|
||||||
|
"male:yaoi",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Mixed : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"mixed:animal on animal",
|
||||||
|
"mixed:body swap",
|
||||||
|
"mixed:ffm threesome",
|
||||||
|
"mixed:frottage",
|
||||||
|
"mixed:group",
|
||||||
|
"mixed:incest",
|
||||||
|
"mixed:mmf threesome",
|
||||||
|
"mixed:mmt threesome",
|
||||||
|
"mixed:mtf threesome",
|
||||||
|
"mixed:multimouth blowjob",
|
||||||
|
"mixed:multiple assjob",
|
||||||
|
"mixed:multiple footjob",
|
||||||
|
"mixed:multiple handjob",
|
||||||
|
"mixed:oyakodon",
|
||||||
|
"mixed:shimaidon",
|
||||||
|
"mixed:ttm threesome",
|
||||||
|
"mixed:twins",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object Other : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"other:3d",
|
||||||
|
"other:already uploaded",
|
||||||
|
"other:anaglyph",
|
||||||
|
"other:animated",
|
||||||
|
"other:anthology",
|
||||||
|
"other:artbook",
|
||||||
|
"other:caption",
|
||||||
|
"other:comic",
|
||||||
|
"other:compilation",
|
||||||
|
"other:dakimakura",
|
||||||
|
"other:figure",
|
||||||
|
"other:forbidden content",
|
||||||
|
"other:full censorship",
|
||||||
|
"other:full color",
|
||||||
|
"other:game sprite",
|
||||||
|
"other:goudoushi",
|
||||||
|
"other:hardcore",
|
||||||
|
"other:how to",
|
||||||
|
"other:incomplete",
|
||||||
|
"other:missing cover",
|
||||||
|
"other:mosaic censorship",
|
||||||
|
"other:multi-work series",
|
||||||
|
"other:multipanel sequence",
|
||||||
|
"other:no penetration",
|
||||||
|
"other:non-h imageset",
|
||||||
|
"other:non-nude",
|
||||||
|
"other:novel",
|
||||||
|
"other:nudity only",
|
||||||
|
"other:out of order",
|
||||||
|
"other:paperchild",
|
||||||
|
"other:poor grammar",
|
||||||
|
"other:realporn",
|
||||||
|
"other:redraw",
|
||||||
|
"other:replaced",
|
||||||
|
"other:rough translation",
|
||||||
|
"other:sample",
|
||||||
|
"other:scanmark",
|
||||||
|
"other:screenshots",
|
||||||
|
"other:sketch lines",
|
||||||
|
"other:stereoscopic",
|
||||||
|
"other:story arc",
|
||||||
|
"other:tankoubon",
|
||||||
|
"other:themeless",
|
||||||
|
"other:time stop",
|
||||||
|
"other:uncensored",
|
||||||
|
"other:variant set",
|
||||||
|
"other:watermarked",
|
||||||
|
"other:webtoon",
|
||||||
|
"other:western cg",
|
||||||
|
"other:western imageset",
|
||||||
|
"other:western non-h",
|
||||||
|
"other:yukkuri",
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
object ReClass : TagList {
|
||||||
|
|
||||||
|
override fun getTags1() = listOf(
|
||||||
|
"reclass:artistcg",
|
||||||
|
"reclass:asianporn",
|
||||||
|
"reclass:cosplay",
|
||||||
|
"reclass:doujinshi",
|
||||||
|
"reclass:gamecg",
|
||||||
|
"reclass:imageset",
|
||||||
|
"reclass:manga",
|
||||||
|
"reclass:misc",
|
||||||
|
"reclass:non-h",
|
||||||
|
"reclass:western",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package exh.eh.tags
|
||||||
|
|
||||||
|
interface TagList {
|
||||||
|
fun getTags1(): List<String>
|
||||||
|
|
||||||
|
fun getTags2(): List<String> = emptyList()
|
||||||
|
|
||||||
|
fun getTags3(): List<String> = emptyList()
|
||||||
|
|
||||||
|
fun getTags4(): List<String> = emptyList()
|
||||||
|
|
||||||
|
fun getTags() = listOf(
|
||||||
|
getTags1(),
|
||||||
|
getTags2(),
|
||||||
|
getTags3(),
|
||||||
|
getTags4(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class MangaDexLoginHelper(val authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
|
class MangaDexLoginHelper(authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
|
||||||
val authService by authServiceLazy
|
private val authService by authServiceLazy
|
||||||
suspend fun isAuthenticated(): Boolean {
|
suspend fun isAuthenticated(): Boolean {
|
||||||
return runCatching { authService.checkToken().isAuthenticated }
|
return runCatching { authService.checkToken().isAuthenticated }
|
||||||
.getOrElse { e ->
|
.getOrElse { e ->
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ import okhttp3.Authenticator
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
|
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
|
||||||
override fun authenticate(route: Route?, response: Response): Request? {
|
override fun authenticate(route: Route?, response: Response): Request? {
|
||||||
xLogI("Detected Auth error ${response.code} on ${response.request.url}")
|
xLogI("Detected Auth error ${response.code} on ${response.request.url}")
|
||||||
|
|
||||||
val token = refreshToken(loginHelper)
|
val token = try {
|
||||||
|
refreshToken(loginHelper)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
return if (token != null) {
|
return if (token != null) {
|
||||||
response.request.newBuilder().header("Authorization", token).build()
|
response.request.newBuilder().header("Authorization", token).build()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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.queries.getAllMergedMangaQuery
|
import eu.kanade.tachiyomi.data.database.queries.getAllMergedMangaQuery
|
||||||
import eu.kanade.tachiyomi.data.database.queries.getMergedChaptersQuery
|
import eu.kanade.tachiyomi.data.database.queries.getMergedChaptersQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaForDownloadingQuery
|
||||||
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaFromUrlQuery
|
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaFromUrlQuery
|
||||||
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaQuery
|
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaQuery
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
@@ -61,6 +62,16 @@ interface MergedQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getMergedMangasForDownloading(mergedMangaId: Long) = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getMergedMangaForDownloadingQuery())
|
||||||
|
.args(mergedMangaId)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getMergedMangas(mergedMangaUrl: String) = db.get()
|
fun getMergedMangas(mergedMangaUrl: String) = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
|
|||||||
@@ -51,16 +51,14 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
val cover = thumbnailUrl
|
val cover = thumbnailUrl
|
||||||
|
|
||||||
// No title bug?
|
// No title bug?
|
||||||
val title = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().get()) {
|
val title = altTitle
|
||||||
altTitle ?: title
|
?.takeIf { Injekt.get<PreferencesHelper>().useJapaneseTitle().get() }
|
||||||
} else {
|
?: title
|
||||||
title
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set artist (if we can find one)
|
// Set artist (if we can find one)
|
||||||
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE).let { tags ->
|
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE)
|
||||||
if (tags.isNotEmpty()) tags.joinToString(transform = { it.name }) else null
|
.ifEmpty { null }
|
||||||
}
|
?.joinToString { it.name }
|
||||||
|
|
||||||
// Copy tags -> genres
|
// Copy tags -> genres
|
||||||
val genres = tagsToGenreList()
|
val genres = tagsToGenreList()
|
||||||
@@ -92,25 +90,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
gId?.let { getString(R.string.id) to it },
|
getItem(gId) { getString(R.string.id) },
|
||||||
gToken?.let { getString(R.string.token) to it },
|
getItem(gToken) { getString(R.string.token) },
|
||||||
exh?.let { getString(R.string.is_exhentai_gallery) to it.toString() },
|
getItem(exh) { getString(R.string.is_exhentai_gallery) },
|
||||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
altTitle?.let { getString(R.string.alt_title) to it },
|
getItem(altTitle) { getString(R.string.alt_title) },
|
||||||
genre?.let { getString(R.string.genre) to it },
|
getItem(genre) { getString(R.string.genre) },
|
||||||
datePosted?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
getItem(datePosted, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||||
parent?.let { getString(R.string.parent) to it },
|
getItem(parent) { getString(R.string.parent) },
|
||||||
visible?.let { getString(R.string.visible) to it },
|
getItem(visible) { getString(R.string.visible) },
|
||||||
language?.let { getString(R.string.language) to it },
|
getItem(language) { getString(R.string.language) },
|
||||||
translated?.let { getString(R.string.translated) to it.toString() },
|
getItem(translated) { getString(R.string.translated) },
|
||||||
size?.let { getString(R.string.gallery_size) to MetadataUtil.humanReadableByteCount(it, true) },
|
getItem(size, { MetadataUtil.humanReadableByteCount(it, true) }) { getString(R.string.gallery_size) },
|
||||||
length?.let { getString(R.string.page_count) to it.toString() },
|
getItem(length) { getString(R.string.page_count) },
|
||||||
favorites?.let { getString(R.string.total_favorites) to it.toString() },
|
getItem(favorites) { getString(R.string.total_favorites) },
|
||||||
ratingCount?.let { getString(R.string.total_ratings) to it.toString() },
|
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
getItem(averageRating) { getString(R.string.average_rating) },
|
||||||
aged.let { getString(R.string.aged) to it.toString() },
|
getItem(aged) { getString(R.string.aged) },
|
||||||
lastUpdateCheck.let { getString(R.string.last_update_check) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
getItem(lastUpdateCheck, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.last_update_check) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +126,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
const val EH_LANGUAGE_NAMESPACE = "language"
|
const val EH_LANGUAGE_NAMESPACE = "language"
|
||||||
const val EH_META_NAMESPACE = "meta"
|
const val EH_META_NAMESPACE = "meta"
|
||||||
const val EH_UPLOADER_NAMESPACE = "uploader"
|
const val EH_UPLOADER_NAMESPACE = "uploader"
|
||||||
|
const val EH_VISIBILITY_NAMESPACE = "visibility"
|
||||||
|
|
||||||
private fun splitGalleryUrl(url: String) =
|
private fun splitGalleryUrl(url: String) =
|
||||||
url.let {
|
url.let {
|
||||||
|
|||||||
@@ -41,10 +41,9 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
path.nullIfEmpty()?.joinToString("/", prefix = "/")
|
getItem(path.nullIfEmpty(), { it.joinToString("/", prefix = "/") }) { getString(R.string.path) },
|
||||||
?.let { getString(R.string.path) to it },
|
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
hbId?.let { getString(R.string.id) to it.toString() },
|
getItem(hbId) { getString(R.string.id) },
|
||||||
hbUrl?.let { getString(R.string.url) to it },
|
getItem(hbUrl) { getString(R.string.url) },
|
||||||
thumbnail?.let { getString(R.string.thumbnail_url) to it },
|
getItem(thumbnail) { getString(R.string.thumbnail_url) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
length?.let { getString(R.string.page_count) to it.toString() },
|
getItem(length) { getString(R.string.page_count) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
hlId?.let { getString(R.string.id) to it },
|
getItem(hlId) { getString(R.string.id) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||||
artists.nullIfEmpty()?.joinToString()?.let { getString(R.string.artist) to it },
|
getItem(artists.nullIfEmpty(), { it.joinToString() }) { getString(R.string.artist) },
|
||||||
genre?.let { getString(R.string.genre) to it },
|
getItem(genre) { getString(R.string.genre) },
|
||||||
language?.let { getString(R.string.language) to it },
|
getItem(language) { getString(R.string.language) },
|
||||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,24 +77,24 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
mdUuid?.let { getString(R.string.id) to it },
|
getItem(mdUuid) { getString(R.string.id) },
|
||||||
// mdUrl?.let { getString(R.string.url) to it },
|
// getItem(mdUrl) { getString(R.string.url) },
|
||||||
cover?.let { getString(R.string.thumbnail_url) to it },
|
getItem(cover) { getString(R.string.thumbnail_url) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
authors?.let { getString(R.string.author) to it.joinToString() },
|
getItem(authors, { it.joinToString() }) { getString(R.string.author) },
|
||||||
artists?.let { getString(R.string.artist) to it.joinToString() },
|
getItem(artists, { it.joinToString() }) { getString(R.string.artist) },
|
||||||
langFlag?.let { getString(R.string.language) to it },
|
getItem(langFlag) { getString(R.string.language) },
|
||||||
lastChapterNumber?.let { getString(R.string.last_chapter_number) to it.toString() },
|
getItem(lastChapterNumber) { getString(R.string.last_chapter_number) },
|
||||||
rating?.let { getString(R.string.average_rating) to it.toString() },
|
getItem(rating) { getString(R.string.average_rating) },
|
||||||
// users?.let { getString(R.string.total_ratings) to it },
|
// getItem(users) { getString(R.string.total_ratings) },
|
||||||
status?.let { getString(R.string.status) to it.toString() },
|
getItem(status) { getString(R.string.status) },
|
||||||
// missing_chapters?.let { getString(R.string.missing_chapters) to it },
|
// getItem(missing_chapters) { getString(R.string.missing_chapters) },
|
||||||
followStatus?.let { getString(R.string.follow_status) to it.toString() },
|
getItem(followStatus) { getString(R.string.follow_status) },
|
||||||
anilistId?.let { getString(R.string.anilist_id) to it },
|
getItem(anilistId) { getString(R.string.anilist_id) },
|
||||||
kitsuId?.let { getString(R.string.kitsu_id) to it },
|
getItem(kitsuId) { getString(R.string.kitsu_id) },
|
||||||
myAnimeListId?.let { getString(R.string.mal_id) to it },
|
getItem(myAnimeListId) { getString(R.string.mal_id) },
|
||||||
mangaUpdatesId?.let { getString(R.string.manga_updates_id) to it },
|
getItem(mangaUpdatesId) { getString(R.string.manga_updates_id) },
|
||||||
animePlanetId?.let { getString(R.string.anime_planet_id) to it },
|
getItem(animePlanetId) { getString(R.string.anime_planet_id) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,17 +88,17 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
nhId?.let { getString(R.string.id) to it.toString() },
|
getItem(nhId) { getString(R.string.id) },
|
||||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) },
|
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) }) { getString(R.string.date_posted) },
|
||||||
favoritesCount?.let { getString(R.string.total_favorites) to it.toString() },
|
getItem(favoritesCount) { getString(R.string.total_favorites) },
|
||||||
mediaId?.let { getString(R.string.media_id) to it },
|
getItem(mediaId) { getString(R.string.media_id) },
|
||||||
japaneseTitle?.let { getString(R.string.japanese_title) to it },
|
getItem(japaneseTitle) { getString(R.string.japanese_title) },
|
||||||
englishTitle?.let { getString(R.string.english_title) to it },
|
getItem(englishTitle) { getString(R.string.english_title) },
|
||||||
shortTitle?.let { getString(R.string.short_title) to it },
|
getItem(shortTitle) { getString(R.string.short_title) },
|
||||||
coverImageType?.let { getString(R.string.cover_image_file_type) to it },
|
getItem(coverImageType) { getString(R.string.cover_image_file_type) },
|
||||||
pageImageTypes.size.let { getString(R.string.page_count) to it.toString() },
|
getItem(pageImageTypes.size) { getString(R.string.page_count) },
|
||||||
thumbnailImageType?.let { getString(R.string.thumbnail_image_file_type) to it },
|
getItem(thumbnailImageType) { getString(R.string.thumbnail_image_file_type) },
|
||||||
scanlator?.let { getString(R.string.scanlator) to it },
|
getItem(scanlator) { getString(R.string.scanlator) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,17 +67,16 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
pvId?.let { getString(R.string.id) to it },
|
getItem(pvId) { getString(R.string.id) },
|
||||||
url?.let { getString(R.string.url) to it },
|
getItem(url) { getString(R.string.url) },
|
||||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
altTitles.nullIfEmpty()?.joinToString()
|
getItem(altTitles.nullIfEmpty(), { it.joinToString() }) { getString(R.string.alt_titles) },
|
||||||
?.let { getString(R.string.alt_titles) to it },
|
getItem(artist) { getString(R.string.artist) },
|
||||||
artist?.let { getString(R.string.artist) to it },
|
getItem(genre) { getString(R.string.genre) },
|
||||||
genre?.let { getString(R.string.genre) to it },
|
getItem(rating) { getString(R.string.average_rating) },
|
||||||
rating?.let { getString(R.string.average_rating) to it.toString() },
|
getItem(status) { getString(R.string.status) },
|
||||||
status?.let { getString(R.string.status) to it },
|
getItem(lang) { getString(R.string.language) },
|
||||||
lang?.let { getString(R.string.language) to it },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,16 +56,16 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
prId?.let { getString(R.string.id) to it.toString() },
|
getItem(prId) { getString(R.string.id) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
altTitle?.let { getString(R.string.alt_title) to it },
|
getItem(altTitle) { getString(R.string.alt_title) },
|
||||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||||
uploaderDisp?.let { getString(R.string.uploader_capital) to it },
|
getItem(uploaderDisp) { getString(R.string.uploader_capital) },
|
||||||
uploader?.let { getString(R.string.uploader) to it },
|
getItem(uploader) { getString(R.string.uploader) },
|
||||||
pages?.let { getString(R.string.page_count) to it.toString() },
|
getItem(pages) { getString(R.string.page_count) },
|
||||||
fileSize?.let { getString(R.string.gallery_size) to it },
|
getItem(fileSize) { getString(R.string.gallery_size) },
|
||||||
ratingCount?.let { getString(R.string.total_ratings) to it.toString() },
|
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
getItem(averageRating) { getString(R.string.average_rating) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,20 +69,20 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
return with(context) {
|
return with(context) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
tmId?.let { getString(R.string.id) to it.toString() },
|
getItem(tmId) { getString(R.string.id) },
|
||||||
title?.let { getString(R.string.title) to it },
|
getItem(title) { getString(R.string.title) },
|
||||||
uploader?.let { getString(R.string.uploader) to it },
|
getItem(uploader) { getString(R.string.uploader) },
|
||||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||||
length?.let { getString(R.string.page_count) to it.toString() },
|
getItem(length) { getString(R.string.page_count) },
|
||||||
ratingString?.let { getString(R.string.rating_string) to it },
|
getItem(ratingString) { getString(R.string.rating_string) },
|
||||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
getItem(averageRating) { getString(R.string.average_rating) },
|
||||||
userRatings?.let { getString(R.string.total_ratings) to it.toString() },
|
getItem(userRatings) { getString(R.string.total_ratings) },
|
||||||
favorites?.let { getString(R.string.total_favorites) to it.toString() },
|
getItem(favorites) { getString(R.string.total_favorites) },
|
||||||
category?.let { getString(R.string.genre) to it },
|
getItem(category) { getString(R.string.genre) },
|
||||||
collection?.let { getString(R.string.collection) to it },
|
getItem(collection) { getString(R.string.collection) },
|
||||||
group?.let { getString(R.string.group) to it },
|
getItem(group) { getString(R.string.group) },
|
||||||
parody.nullIfEmpty()?.joinToString()?.let { getString(R.string.parodies) to it },
|
getItem(parody.nullIfEmpty(), { it.joinToString() }) { getString(R.string.parodies) },
|
||||||
character.nullIfEmpty()?.joinToString()?.let { getString(R.string.characters) to it },
|
getItem(character.nullIfEmpty(), { it.joinToString() }) { getString(R.string.characters) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ abstract class RaisedSearchMetadata {
|
|||||||
if (newTitle != null) titles += RaisedTitle(newTitle, type)
|
if (newTitle != null) titles += RaisedTitle(newTitle, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Any> getItem(
|
||||||
|
item: T?,
|
||||||
|
toString: (T) -> String = Any::toString,
|
||||||
|
block: (T) -> String,
|
||||||
|
): Pair<String, String>? {
|
||||||
|
item ?: return null
|
||||||
|
return block(item) to toString(item)
|
||||||
|
}
|
||||||
|
|
||||||
open fun copyTo(manga: SManga) {
|
open fun copyTo(manga: SManga) {
|
||||||
val infoManga = createMangaInfo(manga.toMangaInfo()).toSManga()
|
val infoManga = createMangaInfo(manga.toMangaInfo()).toSManga()
|
||||||
manga.copyFrom(infoManga)
|
manga.copyFrom(infoManga)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
|
|||||||
|
|
||||||
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
||||||
// TODO make it so captcha doesnt auto open in manga eden while applying universal interceptors
|
// TODO make it so captcha doesnt auto open in manga eden while applying universal interceptors
|
||||||
return if (Injekt.get<PreferencesHelper>().autoSolveCaptcha().get()) ((EH_INTERCEPTORS[sourceId].orEmpty()) + (EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty())).merge()
|
return if (Injekt.get<PreferencesHelper>().autoSolveCaptcha().get()) (EH_INTERCEPTORS[sourceId].orEmpty() + EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty()).merge()
|
||||||
else (EH_INTERCEPTORS[sourceId].orEmpty()).merge()
|
else EH_INTERCEPTORS[sourceId].orEmpty().merge()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<EHInterceptor>.merge(): EHInterceptor {
|
fun List<EHInterceptor>.merge(): EHInterceptor {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||||
|
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:alpha="0.24" android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||||
|
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
|
||||||
|
</selector>
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
android:width="108dp"
|
||||||
<item
|
android:height="108dp"
|
||||||
android:width="36dp"
|
android:viewportWidth="108"
|
||||||
android:height="36dp"
|
android:viewportHeight="108">
|
||||||
android:drawable="@drawable/ic_tachi"
|
<path
|
||||||
android:gravity="center" />
|
android:pathData="M52.2,35c7.9,-0.1 9.9,3.3 12.7,5.1c1.5,0.6 1.2,1.1 1.8,2c0.2,0.2 0.4,0.3 0.7,0.5c2.1,1.2 -0.7,2.7 -1.9,2.8c0.5,-0.3 0.9,-0.4 1.2,-0.8c0,0 0,0 -0.1,0c0,0 0,0 0,-0.1c-1,-0.3 -0.9,0.6 -2,0.4c0.2,0 0.9,-0.4 0.8,-0.9c-0.8,-0.5 -2.2,-1 -3.2,-1c0,0 0,0 0,0.1c2.5,1.7 1.8,3.9 3.2,4.4c-0.8,0 -0.4,0.1 0,0.5c-1,-0.2 -1.1,-1.3 -1.6,-2.3c-0.5,-0.9 -1.1,-2 -1.6,-2c-0.1,0.6 0.1,3.9 1,3.2c-0.7,0.8 0.3,0.5 0.8,0.3c-1.6,1.3 -2.4,0.9 -2.9,-0.9c-0.4,-0.9 -1,-2.3 -1.3,-3.2c-0.2,-0.5 -0.5,-0.9 -0.9,-1.2c-2.9,-2.1 -5,-2.3 -8,-1.8c-0.8,-1.5 -1.9,-2.8 -2.7,-4.2C49.6,35.4 50.7,35 52.2,35z"
|
||||||
</layer-list>
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M41.2,41.4c5.6,10.8 11.5,7.3 20.1,11.4c7.5,4.6 5.7,14.6 -2.1,17.6c0,-1.9 0,-3.9 0,-5.8c3.2,-3.1 1.4,-7.7 -2.5,-8.6c-4.7,-1 -10,-2 -13.1,-4.3C40.2,49.3 39.4,44.9 41.2,41.4z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M48.9,65.7c0,1.7 0.1,3.7 -0.1,5.3c-6.4,-1.8 -11.1,-7.1 -8.5,-13.2c0,0.3 0,0.6 0,1C40.8,62.9 44.4,65.2 48.9,65.7z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M36.7,32l9.5,2.5L54,46.8l3.3,-5c2.4,1 2.5,5.9 3.8,6.6l-1.6,2.4c-7.1,-1.9 -13.2,-2.3 -15.7,-7.4C41.5,39.8 36.7,32 36.7,32z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M61,35.9c1.1,0.7 2.3,1.4 3.2,2.2c0.5,0.5 1,0.8 1.7,1.3c0.3,0.2 0.3,0.1 0.5,0.3l4.8,-7.8l-9.4,2.5L61,35.9z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54.1,76l4.2,-3.4L58.4,58c0,0 -0.3,-0.8 -5.8,-1.7c-1.2,-0.2 -2,-0.5 -2,-0.5l-0.8,-0.2l0.1,17L54.1,76z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
android:background="?attr/colorTertiary"
|
android:background="?attr/colorTertiary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appbar"
|
app:layout_constraintTop_toBottomOf="@+id/appbar"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
|
app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
@@ -73,11 +73,10 @@
|
|||||||
<com.google.android.material.navigationrail.NavigationRailView
|
<com.google.android.material.navigationrail.NavigationRailView
|
||||||
android:id="@+id/side_nav"
|
android:id="@+id/side_nav"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
app:elevation="0dp"
|
android:paddingTop="?attr/actionBarSize"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:elevation="1dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/incognito_mode"
|
|
||||||
app:menu="@menu/main_nav" />
|
app:menu="@menu/main_nav" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
|||||||
@@ -31,57 +31,63 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<HorizontalScrollView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/migration_data_scrollView"
|
android:id="@+id/migration_data_group"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:scrollbars="none"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/data_label">
|
app:layout_constraintTop_toBottomOf="@+id/data_label">
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra"
|
||||||
|
app:flow_horizontalBias="0"
|
||||||
|
app:flow_horizontalGap="8dp"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_verticalGap="2dp"
|
||||||
|
app:flow_wrapMode="chain"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_chapters"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:checked="true"
|
||||||
|
android:text="@string/chapters" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/mig_chapters"
|
android:id="@+id/mig_categories"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:checked="true"
|
||||||
android:checked="true"
|
android:text="@string/categories" />
|
||||||
android:text="@string/chapters" />
|
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/mig_categories"
|
android:id="@+id/mig_tracking"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:checked="true"
|
||||||
android:checked="true"
|
android:text="@string/track" />
|
||||||
android:text="@string/categories" />
|
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/mig_tracking"
|
android:id="@+id/mig_custom_cover"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:checked="true"
|
||||||
android:checked="true"
|
android:text="@string/custom_cover" />
|
||||||
android:text="@string/track" />
|
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/mig_extra"
|
android:id="@+id/mig_extra"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:checked="true"
|
||||||
android:checked="true"
|
android:text="@string/log_extra" />
|
||||||
android:text="@string/log_extra" />
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/migration_data_divider"
|
android:id="@+id/migration_data_divider"
|
||||||
@@ -91,17 +97,18 @@
|
|||||||
android:background="?android:attr/divider"
|
android:background="?android:attr/divider"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/migration_data_scrollView"/>
|
app:layout_constraintTop_toBottomOf="@id/migration_data_group"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/options_label"
|
android:id="@+id/options_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:text="@string/action_settings"
|
android:text="@string/action_settings"
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
android:textColor="?attr/colorPrimary"
|
android:textColor="?attr/colorPrimary"
|
||||||
app:layout_constraintStart_toStartOf="@+id/migration_data_scrollView"
|
app:layout_constraintStart_toStartOf="@+id/migration_data_group"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/migration_data_divider" />
|
app:layout_constraintTop_toBottomOf="@+id/migration_data_divider" />
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
@@ -350,9 +350,10 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/left_page_text"
|
android:id="@+id/left_page_text"
|
||||||
android:layout_width="32dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:minWidth="32dp"
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
tools:text="1" />
|
tools:text="1" />
|
||||||
@@ -371,9 +372,10 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/right_page_text"
|
android:id="@+id/right_page_text"
|
||||||
android:layout_width="32dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:minWidth="32dp"
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
tools:text="15" />
|
tools:text="15" />
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
android:id="@+id/upper_text"
|
android:id="@+id/upper_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
tools:text="Top" />
|
tools:text="Top" />
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
android:id="@+id/warning"
|
android:id="@+id/warning"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
android:id="@+id/lower_text"
|
android:id="@+id/lower_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
tools:text="Bottom" />
|
tools:text="Bottom" />
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user