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 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
|
||||
- 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
|
||||
|
||||
@@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.8.2"
|
||||
Example: "1.8.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
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
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
||||
@@ -33,7 +33,7 @@ body:
|
||||
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).
|
||||
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
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: TAG - Bump version and push tag
|
||||
uses: anothrNick/github-tag-action@1.17.2
|
||||
uses: anothrNick/github-tag-action@1.39.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WITH_V: true
|
||||
|
||||
@@ -32,9 +32,10 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
||||
@@ -28,9 +28,10 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
||||
@@ -25,8 +25,8 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 33
|
||||
versionName = "1.8.2"
|
||||
versionCode = 35
|
||||
versionName = "1.8.4"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -202,6 +202,7 @@ dependencies {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.markwon)
|
||||
|
||||
// Conductor
|
||||
implementation(libs.bundles.conductor)
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
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_MASK
|
||||
@@ -74,7 +75,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
backupCategories(flags),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga),
|
||||
backupSavedSearches(),
|
||||
@@ -111,6 +112,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||
if (byteArray.isEmpty()) {
|
||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||
}
|
||||
|
||||
file.openOutputStream().also {
|
||||
// Force overwrite old file
|
||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||
@@ -149,10 +154,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private fun backupCategories(): List<BackupCategory> {
|
||||
return databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
private fun backupCategories(options: Int): List<BackupCategory> {
|
||||
// Check if user wants category information in backup
|
||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
|
||||
@@ -25,6 +25,19 @@ fun getMergedMangaQuery() =
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
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.
|
||||
* Only works on Android 8+.
|
||||
*/
|
||||
fun onWarning(reason: String, timeout: Long? = null) {
|
||||
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
timeout?.let { setTimeoutAfter(it) }
|
||||
contentIntent?.let { setContentIntent(it) }
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
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.download.model.Download
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
@@ -278,7 +279,8 @@ class Downloader(
|
||||
val maxDownloadsFromSource = queue
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
.maxOf { it.value.size }
|
||||
.maxOfOrNull { it.value.size }
|
||||
?: 0
|
||||
if (
|
||||
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||
@@ -287,6 +289,7 @@ class Downloader(
|
||||
notifier.onWarning(
|
||||
context.getString(R.string.download_queue_size_warning),
|
||||
WARNING_NOTIF_TIMEOUT_MS,
|
||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -474,7 +477,7 @@ class Downloader(
|
||||
// Else read magic numbers.
|
||||
?: 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.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.*
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||
Result.failure()
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
return if (LibraryUpdateService.start(context)) {
|
||||
@@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
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)
|
||||
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
@@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
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() {
|
||||
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||
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)
|
||||
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
@@ -341,6 +342,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
}
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
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.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -207,6 +208,8 @@ class LibraryUpdateService(
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
updateJob?.cancel()
|
||||
// Despite what Android Studio
|
||||
// states this can be null
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
@@ -272,8 +275,7 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* Adds list of manga to be updated.
|
||||
*
|
||||
* @param category the ID of the category to update, or -1 if no category specified.
|
||||
* @param target the target to update.
|
||||
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||
*/
|
||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
@@ -308,17 +310,13 @@ class LibraryUpdateService(
|
||||
when (group) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||
val statuses = loggedServices.associate {
|
||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
||||
}
|
||||
|
||||
libraryManga.filter { manga ->
|
||||
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
||||
statuses[track.sync_id]?.get(track.status)
|
||||
} ?: "not tracked"
|
||||
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
||||
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||
} ?: TrackStatus.OTHER
|
||||
status.int == trackingExtra
|
||||
}
|
||||
}
|
||||
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.
|
||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||
* progress.
|
||||
*
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList() {
|
||||
@@ -389,35 +386,38 @@ class LibraryUpdateService(
|
||||
return@async
|
||||
}
|
||||
|
||||
// Don't continue to update if manga not in library
|
||||
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
|
||||
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
) { mangaWithNotif ->
|
||||
try {
|
||||
when {
|
||||
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
|
||||
}
|
||||
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
|
||||
}
|
||||
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
|
||||
}
|
||||
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
|
||||
|
||||
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
|
||||
|
||||
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
|
||||
|
||||
else -> {
|
||||
// Convert to the manga that contains new chapters
|
||||
val (newChapters, _) = updateManga(manga, loggedServices)
|
||||
val (newChapters, _) = updateManga(mangaWithNotif, loggedServices)
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(mangaWithNotif, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
@@ -436,11 +436,11 @@ class LibraryUpdateService(
|
||||
e.message
|
||||
}
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
failedUpdates.add(mangaWithNotif to errorMessage)
|
||||
}
|
||||
|
||||
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
|
||||
// may don't like it and they could ban the user.
|
||||
// SY -->
|
||||
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
||||
db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
||||
.filterNot { it.downloadChapters }
|
||||
.mapNotNull { it.mangaId } + manga.id!!
|
||||
} else emptyList()
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
val downloadingManga = db.getMergedMangasForDownloading(manga.id!!).executeAsBlocking()
|
||||
.associateBy { it.id!! }
|
||||
chapters.groupBy { it.manga_id }
|
||||
.forEach {
|
||||
downloadManager.downloadChapters(
|
||||
downloadingManga[it.key] ?: return@forEach,
|
||||
chapters,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// 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>> {
|
||||
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
||||
|
||||
var networkSManga: SManga? = null
|
||||
// Update manga details metadata
|
||||
if (preferences.autoUpdateMetadata()) {
|
||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
@@ -506,8 +516,7 @@ class LibraryUpdateService(
|
||||
sManga.thumbnail_url = manga.thumbnail_url
|
||||
}
|
||||
|
||||
manga.copyFrom(sManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
networkSManga = sManga
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -532,7 +541,20 @@ class LibraryUpdateService(
|
||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||
.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() {
|
||||
@@ -555,16 +577,16 @@ class LibraryUpdateService(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
) { mangaWithNotif ->
|
||||
sourceManager.get(mangaWithNotif.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
source.getMangaDetails(mangaWithNotif.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
mangaWithNotif.thumbnail_url = it
|
||||
db.insertManga(mangaWithNotif).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
|
||||
@@ -30,7 +30,7 @@ object Notifications {
|
||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||
const val ID_LIBRARY_ERROR = -102
|
||||
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.
|
||||
|
||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||
const val DEVICE_CHARGING = "ac"
|
||||
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
||||
|
||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||
@@ -28,13 +30,14 @@ object PreferenceValues {
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
LAVENDER(R.string.theme_lavender),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
TAKO(R.string.theme_tako),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
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.viewer.pager.PagerConfig
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
@@ -212,11 +213,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||
|
||||
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||
|
||||
@@ -288,10 +289,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
|
||||
|
||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
@@ -23,16 +22,6 @@ class TrackManager(context: Context) {
|
||||
// SY --> Mangadex from Neko
|
||||
const val MDLIST = 60
|
||||
// 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)
|
||||
@@ -54,17 +43,4 @@ class TrackManager(context: Context) {
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
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 -->
|
||||
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 <--
|
||||
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
return if (BuildConfig.DEBUG) {
|
||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||
@@ -69,7 +70,15 @@ class AppUpdateChecker {
|
||||
} else {
|
||||
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
||||
// 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.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.source.BlacklistedSources
|
||||
import kotlinx.serialization.Serializable
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -22,21 +24,41 @@ internal class ExtensionGithubApi {
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
return withIOContext {
|
||||
val extensions = networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
val githubResponse = if (requiresFallbackSource) null else try {
|
||||
networkService.client
|
||||
.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>>()
|
||||
.toExtensions() /* SY --> */ + preferences.extensionRepos()
|
||||
.get()
|
||||
.flatMap { repoPath ->
|
||||
val url = "$BASE_URL$repoPath/repo/"
|
||||
val url = if (requiresFallbackSource) {
|
||||
"$FALLBACK_BASE_URL$repoPath@repo/"
|
||||
} else {
|
||||
"$BASE_URL$repoPath/repo/"
|
||||
}
|
||||
networkService.client
|
||||
.newCall(GET("${url}index.min.json"))
|
||||
.await()
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(url)
|
||||
.toExtensions(url, repoSource = true)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -85,7 +107,12 @@ internal class ExtensionGithubApi {
|
||||
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
|
||||
.filter {
|
||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||
@@ -106,6 +133,7 @@ internal class ExtensionGithubApi {
|
||||
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
|
||||
// SY -->
|
||||
repoUrl = repoUrl,
|
||||
isRepoSource = repoSource,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
@@ -125,6 +153,14 @@ internal class ExtensionGithubApi {
|
||||
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
return if (requiresFallbackSource) {
|
||||
FALLBACK_REPO_URL_PREFIX
|
||||
} else {
|
||||
REPO_URL_PREFIX
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
|
||||
@@ -134,8 +170,10 @@ internal class ExtensionGithubApi {
|
||||
// SY <--
|
||||
}
|
||||
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
private const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
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
|
||||
private data class ExtensionJsonObject(
|
||||
|
||||
@@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||
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 {
|
||||
"pm install-create -i ${service.packageName} -S $size"
|
||||
"pm install-create -r -i ${service.packageName} -S $size"
|
||||
}
|
||||
val createResult = exec(createCommand)
|
||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||
|
||||
@@ -48,6 +48,7 @@ sealed class Extension {
|
||||
val iconUrl: String,
|
||||
// SY -->
|
||||
val repoUrl: String,
|
||||
val isRepoSource: Boolean,
|
||||
// SY <--
|
||||
) : Extension()
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
|
||||
const val PREF_DOH_GOOGLE = 2
|
||||
const val PREF_DOH_ADGUARD = 3
|
||||
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(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
@@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
|
||||
)
|
||||
.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)
|
||||
.connectTimeout(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
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
@@ -46,6 +46,10 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,11 @@ private class RateLimitInterceptor(
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
// Ignore canceled calls, otherwise they would jam the queue
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
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) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
|
||||
+11
-1
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
|
||||
private val host = httpUrl.host
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
@@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.Archive
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@@ -35,50 +37,10 @@ import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
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) }
|
||||
}
|
||||
}
|
||||
class LocalSource(
|
||||
private val context: Context,
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
@@ -86,86 +48,100 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
// SY <--
|
||||
|
||||
override val id = ID
|
||||
override val name = context.getString(R.string.local_source)
|
||||
override val lang = "other"
|
||||
override val supportsLatest = true
|
||||
override val name: String = context.getString(R.string.local_source)
|
||||
|
||||
override val id: Long = ID
|
||||
|
||||
override val lang: String = "other"
|
||||
|
||||
override fun toString() = name
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
// Browse related
|
||||
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> {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
// SY -->
|
||||
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
||||
// SY <--
|
||||
|
||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
var mangaDirs = baseDirs
|
||||
.asSequence()
|
||||
.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 }
|
||||
var mangaDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// Filter by query or last modified
|
||||
mangaDirs = mangaDirs.filter {
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
break
|
||||
}
|
||||
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sManga = this
|
||||
val mangaInfo = this.toMangaInfo()
|
||||
runBlocking {
|
||||
val chapters = getChapterList(mangaInfo)
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last().toSChapter()
|
||||
val format = getFormat(chapter)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(sManga)
|
||||
}
|
||||
}
|
||||
// Fetch chapters of all the manga
|
||||
mangas.forEach { manga ->
|
||||
val mangaInfo = manga.toMangaInfo()
|
||||
runBlocking {
|
||||
val chapters = getChapterList(mangaInfo)
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last().toSChapter()
|
||||
val format = getFormat(chapter)
|
||||
|
||||
// Copy the cover from the first chapter found.
|
||||
if (thumbnail_url == null) {
|
||||
try {
|
||||
val dest = updateCover(chapter, sManga)
|
||||
thumbnail_url = dest?.absolutePath
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(manga)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 <--
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||
val localDetails = getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||
.flatten()
|
||||
var mangaInfo = manga
|
||||
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
|
||||
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) }
|
||||
|
||||
return if (localDetails != null) {
|
||||
if (localDetails != null) {
|
||||
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
||||
|
||||
manga.copy(
|
||||
title = mangaJson.title ?: manga.title,
|
||||
author = mangaJson.author ?: manga.author,
|
||||
artist = mangaJson.artist ?: manga.artist,
|
||||
description = mangaJson.description ?: manga.description,
|
||||
genres = mangaJson.genre ?: manga.genres,
|
||||
status = mangaJson.status ?: manga.status,
|
||||
mangaInfo = mangaInfo.copy(
|
||||
title = mangaJson.title ?: mangaInfo.title,
|
||||
author = mangaJson.author ?: mangaInfo.author,
|
||||
artist = mangaJson.artist ?: mangaInfo.artist,
|
||||
description = mangaJson.description ?: mangaInfo.description,
|
||||
genres = mangaJson.genre ?: mangaInfo.genres,
|
||||
status = mangaJson.status ?: mangaInfo.status,
|
||||
)
|
||||
} else {
|
||||
manga
|
||||
}
|
||||
|
||||
return mangaInfo
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
val sManga = manga.toSManga()
|
||||
|
||||
val chapters = getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||
.flatten()
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
return getMangaDirsFiles(manga.key, baseDirsFile)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
@@ -243,14 +225,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
|
||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||
|
||||
val format = getFormat(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
}
|
||||
}
|
||||
|
||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
.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 {
|
||||
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 {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(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")
|
||||
|
||||
@@ -419,6 +419,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
// EXH <--
|
||||
|
||||
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_META_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_NORMAL
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
||||
@@ -688,6 +689,9 @@ class EHentai(
|
||||
uploader?.let {
|
||||
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 usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
||||
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 {
|
||||
MangaDexService(client)
|
||||
|
||||
@@ -21,11 +21,12 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import exh.log.xLogW
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.util.executeOnIO
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
@@ -63,18 +64,27 @@ class MergedSource : HttpSource() {
|
||||
|
||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||
return withIOContext {
|
||||
val mergedManga = db.getManga(manga.key, id).executeAsBlocking() ?: throw Exception("merged manga not in db")
|
||||
val mangaReferences = db.getMergedMangaReferences(mergedManga.id ?: throw Exception("merged manga id is null")).executeOnIO()
|
||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
||||
if (mangaReferences.size == 1 &&
|
||||
run {
|
||||
val mangaReference = mangaReferences.firstOrNull()
|
||||
mangaReference == null || mangaReference.mangaSourceId == MERGED_SOURCE_ID
|
||||
val mergedManga = db.getManga(manga.key, id).executeAsBlocking()
|
||||
?: throw Exception("merged manga not in db")
|
||||
val mangaReferences = db.getMergedMangaReferences(mergedManga.id!!).executeAsBlocking()
|
||||
.apply {
|
||||
if (isEmpty()) {
|
||||
throw IllegalArgumentException(
|
||||
"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 dbManga = mangaInfoReference?.let { db.getManga(it.mangaUrl, it.mangaSourceId).executeOnIO()?.toMangaInfo() }
|
||||
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga }
|
||||
?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
||||
val dbManga = mangaInfoReference?.run {
|
||||
db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()?.toMangaInfo()
|
||||
}
|
||||
(dbManga ?: mergedManga.toMangaInfo()).copy(
|
||||
key = manga.key,
|
||||
)
|
||||
@@ -143,41 +153,50 @@ class MergedSource : HttpSource() {
|
||||
|
||||
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
|
||||
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 semaphore = Semaphore(5)
|
||||
var exception: Exception? = null
|
||||
return supervisorScope {
|
||||
mangaReferences
|
||||
.map {
|
||||
.groupBy(MergedMangaReference::mangaSourceId)
|
||||
.minus(MERGED_SOURCE_ID)
|
||||
.map { (_, values) ->
|
||||
async {
|
||||
try {
|
||||
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null
|
||||
val (source, loadedManga, reference) =
|
||||
it.load(db, sourceManager)
|
||||
if (loadedManga != null && reference.getChapterUpdates) {
|
||||
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
val results =
|
||||
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
||||
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||
downloadManager.downloadChapters(
|
||||
loadedManga,
|
||||
results.first,
|
||||
)
|
||||
semaphore.withPermit {
|
||||
values.map {
|
||||
try {
|
||||
val (source, loadedManga, reference) =
|
||||
it.load(db, sourceManager)
|
||||
if (loadedManga != null && reference.getChapterUpdates) {
|
||||
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
||||
.map(ChapterInfo::toSChapter)
|
||||
val results =
|
||||
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
||||
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||
downloadManager.downloadChapters(
|
||||
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()
|
||||
.flatten()
|
||||
.let { pairs ->
|
||||
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 {
|
||||
var manga = db.getManga(mangaUrl, mangaSourceId).executeOnIO()
|
||||
var manga = db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()
|
||||
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
||||
if (manga == null) {
|
||||
manga = Manga.create(mangaSourceId).apply {
|
||||
|
||||
@@ -20,6 +20,9 @@ interface ThemingDelegate {
|
||||
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||
}
|
||||
PreferenceValues.AppTheme.LAVENDER -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Lavender
|
||||
}
|
||||
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.InstallStep
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@@ -57,15 +56,14 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
// SY -->
|
||||
private fun String.plusRepo(extension: Extension): String {
|
||||
return if (extension is Extension.Available) {
|
||||
when (extension.repoUrl) {
|
||||
REPO_URL_PREFIX -> this
|
||||
else -> {
|
||||
if (isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
this + " • "
|
||||
} + itemView.context.getString(R.string.repo_source)
|
||||
}
|
||||
if (!extension.isRepoSource) {
|
||||
this
|
||||
} else {
|
||||
if (isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
"$this • "
|
||||
} + itemView.context.getString(R.string.repo_source)
|
||||
}
|
||||
} else this
|
||||
}
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
object MigrationFlags {
|
||||
|
||||
const val CHAPTERS = 0b0001
|
||||
const val CATEGORIES = 0b0010
|
||||
const val TRACK = 0b0100
|
||||
const val EXTRA = 0b1000
|
||||
|
||||
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)
|
||||
const val CHAPTERS = 0b00001
|
||||
const val CATEGORIES = 0b00010
|
||||
const val TRACK = 0b00100
|
||||
const val CUSTOM_COVER = 0b01000
|
||||
const val EXTRA = 0b10000
|
||||
|
||||
fun hasChapters(value: Int): Boolean {
|
||||
return value and CHAPTERS != 0
|
||||
@@ -29,15 +20,11 @@ object MigrationFlags {
|
||||
return value and TRACK != 0
|
||||
}
|
||||
|
||||
fun hasCustomCover(value: Int): Boolean {
|
||||
return value and CUSTOM_COVER != 0
|
||||
}
|
||||
|
||||
fun hasExtra(value: Int): Boolean {
|
||||
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.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
||||
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
||||
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||
|
||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
|
||||
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.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||
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
|
||||
preferences.migrateFlags().set(flags)
|
||||
}
|
||||
|
||||
+13
-4
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||
|
||||
import android.view.MenuItem
|
||||
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.models.Chapter
|
||||
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.preference.PreferencesHelper
|
||||
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.withIOContext
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -19,6 +21,7 @@ class MigrationProcessAdapter(
|
||||
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
var items: List<MigrationProcessItem> = emptyList()
|
||||
|
||||
@@ -148,11 +151,17 @@ class MigrationProcessAdapter(
|
||||
// Update track
|
||||
if (MigrationFlags.hasTracks(flags)) {
|
||||
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
||||
for (track in tracks) {
|
||||
track.id = null
|
||||
track.manga_id = manga.id!!
|
||||
if (tracks.isNotEmpty()) {
|
||||
tracks.forEach { track ->
|
||||
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
|
||||
if (MigrationFlags.hasExtra(flags)) {
|
||||
|
||||
@@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
||||
else -> throw Exception("Unknown state")
|
||||
},
|
||||
)?.apply {
|
||||
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) {
|
||||
view.context.getResourceColor(R.attr.colorAccent)
|
||||
} else {
|
||||
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
|
||||
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
||||
} else {
|
||||
view.context.getResourceColor(R.attr.colorPrimary)
|
||||
}
|
||||
|
||||
setTint(color)
|
||||
|
||||
@@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
||||
view.popupMenu(
|
||||
menuRes = R.menu.download_single,
|
||||
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 =
|
||||
bindingAdapterPosition != adapter.itemCount - 1
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
@@ -345,8 +344,10 @@ class LibraryController(
|
||||
onTabsSettingsChanged(firstLaunch = true)
|
||||
|
||||
// Delay the scroll position to allow the view to be properly measured.
|
||||
view.doOnAttach {
|
||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||
view.post {
|
||||
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.
|
||||
|
||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -42,7 +43,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -830,7 +830,7 @@ class LibraryPresenter(
|
||||
SManga.ONGOING to context.getString(R.string.ongoing),
|
||||
SManga.LICENSED to context.getString(R.string.licensed),
|
||||
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.COMPLETED to context.getString(R.string.completed),
|
||||
SManga.UNKNOWN to context.getString(R.string.unknown),
|
||||
@@ -848,15 +848,9 @@ class LibraryPresenter(
|
||||
.let(grouping::putAll)
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
grouping.putAll(
|
||||
listOf(
|
||||
TrackManager.READING to context.getString(R.string.reading),
|
||||
TrackManager.REPEATING to context.getString(R.string.repeating),
|
||||
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),
|
||||
TrackStatus.values()
|
||||
.map { it.int to context.getString(it.res) }
|
||||
.associateBy(Pair<Int, *>::first),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -865,21 +859,12 @@ class LibraryPresenter(
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||
val statuses = loggedServices.associate {
|
||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
||||
}
|
||||
libraryManga.forEach { libraryItem ->
|
||||
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
||||
statuses[track.sync_id]?.get(track.status)
|
||||
} ?: "not tracked"
|
||||
val group = grouping.values.find { (statusInt) ->
|
||||
statusInt == (trackManager.trackMap[status] ?: TrackManager.OTHER)
|
||||
}
|
||||
if (group != null) {
|
||||
map.getOrPut(group.first) { mutableListOf() } += libraryItem
|
||||
} else {
|
||||
map.getOrPut(7) { mutableListOf() } += libraryItem
|
||||
}
|
||||
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||
} ?: TrackStatus.OTHER
|
||||
|
||||
map.getOrPut(status.int) { mutableListOf() } += libraryItem
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
|
||||
@@ -1118,7 +1118,7 @@ class MangaController :
|
||||
chaptersHeader.setNumChapters(chapters.size)
|
||||
|
||||
val adapter = chaptersAdapter ?: return
|
||||
adapter.updateDataSet(presenter.cleanChapterNames(chapters))
|
||||
adapter.updateDataSet(chapters)
|
||||
|
||||
if (selectedChapters.isNotEmpty()) {
|
||||
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.
|
||||
*/
|
||||
@@ -1281,38 +1270,3 @@ class MangaPresenter(
|
||||
|
||||
// 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.view.isVisible
|
||||
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.databinding.ChaptersItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
@@ -37,6 +38,8 @@ class ChapterHolder(
|
||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
// TODO: show cleaned name consistently around the app
|
||||
// else -> cleanChapterName(chapter, manga)
|
||||
}
|
||||
|
||||
// Set correct text color
|
||||
@@ -85,4 +88,47 @@ class ChapterHolder(
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
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 -> {
|
||||
activity?.toast(R.string.update_check_no_new_updates)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
activity?.toast(error.message)
|
||||
|
||||
@@ -2,35 +2,58 @@ package eu.kanade.tachiyomi.ui.more
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
||||
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) {
|
||||
|
||||
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 {
|
||||
val releaseBody = args.getString(BODY_KEY)!!
|
||||
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
|
||||
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
|
||||
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.update_check_notification_update_available)
|
||||
.setMessage(args.getString(BODY_KEY) ?: "")
|
||||
.setMessage(info)
|
||||
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
||||
val appContext = applicationContext
|
||||
if (appContext != null) {
|
||||
applicationContext?.let { context ->
|
||||
// Start download
|
||||
val url = args.getString(URL_KEY) ?: ""
|
||||
AppUpdateService.start(appContext, url)
|
||||
val url = args.getString(DOWNLOAD_URL_KEY)!!
|
||||
AppUpdateService.start(context, url)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.update_check_ignore, null)
|
||||
.setNeutralButton(R.string.update_check_open) { _, _ ->
|
||||
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
|
||||
}
|
||||
.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 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.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
@@ -99,16 +101,14 @@ import exh.source.isEhBasedSource
|
||||
import exh.util.defaultReaderType
|
||||
import exh.util.floor
|
||||
import exh.util.mangaType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
@@ -165,8 +165,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
// SY -->
|
||||
private var ehUtilsVisible = false
|
||||
|
||||
private val autoScrollFlow = MutableSharedFlow<Unit>()
|
||||
private var autoScrollJob: Job? = null
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
private var lastShiftDoubleState: Boolean? = null
|
||||
@@ -264,19 +262,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
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 <--
|
||||
|
||||
/**
|
||||
@@ -291,10 +276,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
readingModeToast?.cancel()
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
// SY -->
|
||||
autoScrollJob?.cancel()
|
||||
autoScrollJob = null
|
||||
// SY <--
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,6 +305,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
presenter.saveProgress()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
||||
* Helps with rotations.
|
||||
@@ -716,31 +702,34 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
)
|
||||
|
||||
binding.ehAutoscroll.checkedChanges()
|
||||
.onEach {
|
||||
setupAutoscroll(
|
||||
if (it) {
|
||||
preferences.autoscrollInterval().get().toDouble()
|
||||
} else {
|
||||
-1.0
|
||||
},
|
||||
)
|
||||
.combine(binding.ehAutoscrollFreq.textChanges()) { checked, text ->
|
||||
checked to text
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
binding.ehAutoscrollFreq.textChanges()
|
||||
.onEach {
|
||||
val parsed = it.toString().toDoubleOrNull()
|
||||
.mapLatest { (checked, text) ->
|
||||
val parsed = text.toString().toDoubleOrNull()
|
||||
|
||||
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
||||
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
|
||||
preferences.autoscrollInterval().set(-1f)
|
||||
binding.ehAutoscroll.isEnabled = false
|
||||
setupAutoscroll(-1.0)
|
||||
} else {
|
||||
binding.ehAutoscrollFreq.error = null
|
||||
preferences.autoscrollInterval().set(parsed.toFloat())
|
||||
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)
|
||||
@@ -844,15 +833,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
.show()
|
||||
}
|
||||
.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? {
|
||||
|
||||
@@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.ColorInt
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
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.reader.chapter.ReaderChapterItem
|
||||
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.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
@@ -64,6 +63,7 @@ import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -425,6 +425,14 @@ class ReaderPresenter(
|
||||
* that the user doesn't have to wait too long to continue reading.
|
||||
*/
|
||||
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) {
|
||||
return
|
||||
}
|
||||
@@ -549,6 +557,10 @@ class ReaderPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
fun saveProgress() {
|
||||
getCurrentChapter()?.let { onChapterChanged(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the activity to preload the given [chapter].
|
||||
*/
|
||||
@@ -761,7 +773,7 @@ class ReaderPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveImages(
|
||||
private fun saveImages(
|
||||
page1: ReaderPage,
|
||||
page2: ReaderPage,
|
||||
isLTR: Boolean,
|
||||
@@ -773,11 +785,8 @@ class ReaderPresenter(
|
||||
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
|
||||
val stream2 = page2.stream!!
|
||||
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
|
||||
val imageBytes = stream1().readBytes()
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
|
||||
val imageBytes2 = stream2().readBytes()
|
||||
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
||||
val imageBitmap = ImageDecoder.newInstance(stream1())?.decode()!!
|
||||
val imageBitmap2 = ImageDecoder.newInstance(stream2())?.decode()!!
|
||||
|
||||
val chapter = page1.chapter.chapter
|
||||
|
||||
@@ -872,20 +881,22 @@ class ReaderPresenter(
|
||||
|
||||
Observable
|
||||
.fromCallable {
|
||||
if (manga.isLocal()) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
manga.updateCoverLastModified(db)
|
||||
R.string.cover_updated
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
if (manga.favorite) {
|
||||
coverCache.setCustomCoverToCache(manga, stream())
|
||||
stream().use {
|
||||
if (manga.isLocal()) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, it)
|
||||
manga.updateCoverLastModified(db)
|
||||
coverCache.clearMemoryCache()
|
||||
SetAsCoverResult.Success
|
||||
} 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.Next -> bindNextChapterTransition(transition)
|
||||
}
|
||||
|
||||
missingChapterWarning(transition)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
@@ -24,6 +23,7 @@ import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -366,13 +366,17 @@ class PagerPageHolder(
|
||||
}
|
||||
val imageBytes = imageStream.readBytes()
|
||||
val imageBitmap = try {
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
ImageDecoder.newInstance(imageBytes.inputStream())?.decode()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||
null
|
||||
}
|
||||
if (imageBitmap == null) {
|
||||
imageStream2.close()
|
||||
imageStream.close()
|
||||
page.fullPage = true
|
||||
splitDoublePages()
|
||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||
return imageBytes.inputStream()
|
||||
}
|
||||
viewer.scope.launchUI { progressIndicator.setProgress(96) }
|
||||
@@ -389,14 +393,18 @@ class PagerPageHolder(
|
||||
|
||||
val imageBytes2 = imageStream2.readBytes()
|
||||
val imageBitmap2 = try {
|
||||
BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
||||
ImageDecoder.newInstance(imageBytes2.inputStream())?.decode()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||
null
|
||||
}
|
||||
if (imageBitmap2 == null) {
|
||||
imageStream2.close()
|
||||
imageStream.close()
|
||||
extraPage?.fullPage = true
|
||||
page.isolatedPage = true
|
||||
splitDoublePages()
|
||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||
return imageBytes.inputStream()
|
||||
}
|
||||
viewer.scope.launchUI { progressIndicator.setProgress(97) }
|
||||
|
||||
@@ -67,9 +67,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
awaitingIdleViewerChapters?.let {
|
||||
setChaptersDoubleShift(it)
|
||||
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||
setChaptersDoubleShift(viewerChapters)
|
||||
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.gravity = Gravity.CENTER
|
||||
|
||||
val paddingVertical = 48.dpToPx
|
||||
val paddingVertical = 128.dpToPx
|
||||
val paddingHorizontal = 32.dpToPx
|
||||
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
|
||||
|
||||
@@ -104,6 +104,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
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) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
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" }
|
||||
val position = adapter.items.indexOf(page)
|
||||
if (position != -1) {
|
||||
recycler.scrollToPosition(position)
|
||||
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
||||
onScrolled(pos = position)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.preference.PreferenceValues
|
||||
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_ALIDNS
|
||||
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_QUAD101
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
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.system.DeviceUtil
|
||||
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.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.debug.SettingsDebugController
|
||||
import exh.log.EHLogLevel
|
||||
@@ -57,10 +65,12 @@ import exh.source.BlacklistedSources
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
import kotlinx.coroutines.Job
|
||||
import logcat.LogPriority
|
||||
import rikka.sui.Sui
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
class SettingsAdvancedController : SettingsController() {
|
||||
@@ -87,7 +97,7 @@ class SettingsAdvancedController : SettingsController() {
|
||||
key = Keys.verboseLogging
|
||||
titleRes = R.string.pref_verbose_logging
|
||||
summaryRes = R.string.pref_verbose_logging_summary
|
||||
defaultValue = false
|
||||
defaultValue = isDevFlavor
|
||||
|
||||
onChange {
|
||||
activity?.toast(R.string.requires_app_restart)
|
||||
@@ -170,6 +180,12 @@ class SettingsAdvancedController : SettingsController() {
|
||||
activity?.toast(R.string.cookies_cleared)
|
||||
}
|
||||
}
|
||||
preference {
|
||||
key = "pref_clear_webview_data"
|
||||
titleRes = R.string.pref_clear_webview_data
|
||||
|
||||
onClick { clearWebViewData() }
|
||||
}
|
||||
intListPreference {
|
||||
key = Keys.dohProvider
|
||||
titleRes = R.string.pref_dns_over_https
|
||||
@@ -179,6 +195,10 @@ class SettingsAdvancedController : SettingsController() {
|
||||
"Google",
|
||||
"AdGuard",
|
||||
"Quad9",
|
||||
"AliDNS",
|
||||
"DNSPod",
|
||||
"360",
|
||||
"Quad 101",
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"-1",
|
||||
@@ -186,6 +206,10 @@ class SettingsAdvancedController : SettingsController() {
|
||||
PREF_DOH_GOOGLE.toString(),
|
||||
PREF_DOH_ADGUARD.toString(),
|
||||
PREF_DOH_QUAD9.toString(),
|
||||
PREF_DOH_ALIDNS.toString(),
|
||||
PREF_DOH_DNSPOD.toString(),
|
||||
PREF_DOH_360.toString(),
|
||||
PREF_DOH_QUAD101.toString(),
|
||||
)
|
||||
defaultValue = "-1"
|
||||
summary = "%s"
|
||||
@@ -486,11 +510,30 @@ class SettingsAdvancedController : SettingsController() {
|
||||
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
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 {
|
||||
// SY -->
|
||||
private var job: Job? = null
|
||||
|
||||
@@ -125,20 +125,20 @@ class SettingsDownloadController : SettingsController() {
|
||||
titleRes = R.string.pref_category_auto_download
|
||||
|
||||
switchPreference {
|
||||
bindTo(preferences.downloadNew())
|
||||
bindTo(preferences.downloadNewChapter())
|
||||
titleRes = R.string.pref_download_new
|
||||
}
|
||||
preference {
|
||||
bindTo(preferences.downloadNewCategories())
|
||||
bindTo(preferences.downloadNewChapterCategories())
|
||||
titleRes = R.string.categories
|
||||
onClick {
|
||||
DownloadCategoriesDialog().showDialog(router)
|
||||
}
|
||||
|
||||
visibleIf(preferences.downloadNew()) { it }
|
||||
visibleIf(preferences.downloadNewChapter()) { it }
|
||||
|
||||
fun updateSummary() {
|
||||
val selectedCategories = preferences.downloadNewCategories().get()
|
||||
val selectedCategories = preferences.downloadNewChapterCategories().get()
|
||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||
.sortedBy { it.order }
|
||||
val includedItemsText = if (selectedCategories.isEmpty()) {
|
||||
@@ -147,7 +147,7 @@ class SettingsDownloadController : SettingsController() {
|
||||
selectedCategories.joinToString { it.name }
|
||||
}
|
||||
|
||||
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
|
||||
val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
|
||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||
.sortedBy { it.order }
|
||||
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
||||
@@ -163,10 +163,10 @@ class SettingsDownloadController : SettingsController() {
|
||||
}
|
||||
}
|
||||
|
||||
preferences.downloadNewCategories().asFlow()
|
||||
preferences.downloadNewChapterCategories().asFlow()
|
||||
.onEach { updateSummary() }
|
||||
.launchIn(viewScope)
|
||||
preferences.downloadNewCategoriesExclude().asFlow()
|
||||
preferences.downloadNewChapterCategoriesExclude().asFlow()
|
||||
.onEach { updateSummary() }
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
@@ -254,8 +254,8 @@ class SettingsDownloadController : SettingsController() {
|
||||
var selected = categories
|
||||
.map {
|
||||
when (it.id.toString()) {
|
||||
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||
in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||
in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}
|
||||
@@ -282,8 +282,8 @@ class SettingsDownloadController : SettingsController() {
|
||||
.map { categories[it].id.toString() }
|
||||
.toSet()
|
||||
|
||||
preferences.downloadNewCategories().set(included)
|
||||
preferences.downloadNewCategoriesExclude().set(excluded)
|
||||
preferences.downloadNewChapterCategories().set(included)
|
||||
preferences.downloadNewChapterCategoriesExclude().set(excluded)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
||||
@@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
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_NETWORK_NOT_METERED
|
||||
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_NON_COMPLETED
|
||||
@@ -185,8 +187,8 @@ class SettingsLibraryController : SettingsController() {
|
||||
multiSelectListPreference {
|
||||
bindTo(preferences.libraryUpdateDeviceRestriction())
|
||||
titleRes = R.string.pref_library_update_restriction
|
||||
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
|
||||
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_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_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
|
||||
|
||||
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
||||
|
||||
@@ -202,7 +204,9 @@ class SettingsLibraryController : SettingsController() {
|
||||
.map {
|
||||
when (it) {
|
||||
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_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class ClearDatabaseController :
|
||||
|
||||
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
recycler = binding.recycler
|
||||
|
||||
@@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
if (!favorite) return false
|
||||
|
||||
// Boolean to determine if user wants to automatically download new chapters.
|
||||
val downloadNew = prefs.downloadNew().get()
|
||||
if (!downloadNew) return false
|
||||
val downloadNewChapter = prefs.downloadNewChapter().get()
|
||||
if (!downloadNewChapter) return false
|
||||
|
||||
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
|
||||
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
|
||||
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
|
||||
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
|
||||
|
||||
// Default: download from all categories
|
||||
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true
|
||||
// Default: Download from all categories
|
||||
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
|
||||
|
||||
// Get all categories, else default category (0)
|
||||
val categoriesForManga =
|
||||
@@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||
|
||||
// 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
|
||||
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 java.util.Date
|
||||
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.
|
||||
@@ -60,6 +61,9 @@ fun syncChaptersWithSource(
|
||||
}
|
||||
}
|
||||
|
||||
var maxTimestamp = 0L // in previous chapters to add
|
||||
val rightNow = Date().time
|
||||
|
||||
for (sourceChapter in sourceChapters) {
|
||||
// This forces metadata update for the main viewable things in the chapter list.
|
||||
if (source is HttpSource) {
|
||||
@@ -73,7 +77,9 @@ fun syncChaptersWithSource(
|
||||
// Add the chapter if not in db already, or update if the metadata changed.
|
||||
if (dbChapter == null) {
|
||||
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)
|
||||
} else {
|
||||
@@ -98,6 +104,7 @@ fun syncChaptersWithSource(
|
||||
return Pair(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
// Keep it a List instead of a Set. See #6372.
|
||||
val readded = mutableListOf<Chapter>()
|
||||
|
||||
db.inTransaction {
|
||||
@@ -170,6 +177,7 @@ fun syncChaptersWithSource(
|
||||
db.updateLastUpdated(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
@Suppress("ConvertArgumentToSet")
|
||||
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>()!!
|
||||
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) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
toast(R.string.clipboard_copy_error)
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
@@ -59,6 +60,12 @@ object ImageUtil {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getExtensionFromMimeType(mime: String?): String {
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
|
||||
?: "jpg"
|
||||
}
|
||||
|
||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||
try {
|
||||
val type = getImageType(stream) ?: return false
|
||||
@@ -396,6 +403,12 @@ object ImageUtil {
|
||||
private fun Int.isWhite(): Boolean =
|
||||
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(
|
||||
imageBitmap: Bitmap,
|
||||
imageBitmap2: Bitmap,
|
||||
|
||||
@@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
* @param context any context.
|
||||
* @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 {
|
||||
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) {
|
||||
context.getThemeColor(R.attr.colorControlNormal)
|
||||
} else {
|
||||
context.getThemeColor(R.attr.colorAccent)
|
||||
context.getThemeColor(R.attr.colorPrimary)
|
||||
}
|
||||
if (tint != 0) {
|
||||
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package exh
|
||||
|
||||
import android.content.Context
|
||||
@@ -309,7 +311,6 @@ object EXHMigrations {
|
||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val newSortingMode = when (oldSortingMode) {
|
||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||
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 kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MangaDexLoginHelper(val authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
|
||||
val authService by authServiceLazy
|
||||
class MangaDexLoginHelper(authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
|
||||
private val authService by authServiceLazy
|
||||
suspend fun isAuthenticated(): Boolean {
|
||||
return runCatching { authService.checkToken().isAuthenticated }
|
||||
.getOrElse { e ->
|
||||
|
||||
@@ -7,12 +7,17 @@ import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import java.io.IOException
|
||||
|
||||
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
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) {
|
||||
response.request.newBuilder().header("Authorization", token).build()
|
||||
} 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.queries.getAllMergedMangaQuery
|
||||
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.getMergedMangaQuery
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
@@ -61,6 +62,16 @@ interface MergedQueries : DbProvider {
|
||||
)
|
||||
.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()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
|
||||
@@ -51,16 +51,14 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
val cover = thumbnailUrl
|
||||
|
||||
// No title bug?
|
||||
val title = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().get()) {
|
||||
altTitle ?: title
|
||||
} else {
|
||||
title
|
||||
}
|
||||
val title = altTitle
|
||||
?.takeIf { Injekt.get<PreferencesHelper>().useJapaneseTitle().get() }
|
||||
?: title
|
||||
|
||||
// Set artist (if we can find one)
|
||||
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE).let { tags ->
|
||||
if (tags.isNotEmpty()) tags.joinToString(transform = { it.name }) else null
|
||||
}
|
||||
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE)
|
||||
.ifEmpty { null }
|
||||
?.joinToString { it.name }
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreList()
|
||||
@@ -92,25 +90,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
gId?.let { getString(R.string.id) to it },
|
||||
gToken?.let { getString(R.string.token) to it },
|
||||
exh?.let { getString(R.string.is_exhentai_gallery) to it.toString() },
|
||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
altTitle?.let { getString(R.string.alt_title) to it },
|
||||
genre?.let { getString(R.string.genre) to it },
|
||||
datePosted?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
||||
parent?.let { getString(R.string.parent) to it },
|
||||
visible?.let { getString(R.string.visible) to it },
|
||||
language?.let { getString(R.string.language) to it },
|
||||
translated?.let { getString(R.string.translated) to it.toString() },
|
||||
size?.let { getString(R.string.gallery_size) to MetadataUtil.humanReadableByteCount(it, true) },
|
||||
length?.let { getString(R.string.page_count) to it.toString() },
|
||||
favorites?.let { getString(R.string.total_favorites) to it.toString() },
|
||||
ratingCount?.let { getString(R.string.total_ratings) to it.toString() },
|
||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
||||
aged.let { getString(R.string.aged) to it.toString() },
|
||||
lastUpdateCheck.let { getString(R.string.last_update_check) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
||||
getItem(gId) { getString(R.string.id) },
|
||||
getItem(gToken) { getString(R.string.token) },
|
||||
getItem(exh) { getString(R.string.is_exhentai_gallery) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitle) { getString(R.string.alt_title) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(datePosted, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||
getItem(parent) { getString(R.string.parent) },
|
||||
getItem(visible) { getString(R.string.visible) },
|
||||
getItem(language) { getString(R.string.language) },
|
||||
getItem(translated) { getString(R.string.translated) },
|
||||
getItem(size, { MetadataUtil.humanReadableByteCount(it, true) }) { getString(R.string.gallery_size) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
getItem(favorites) { getString(R.string.total_favorites) },
|
||||
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
getItem(aged) { getString(R.string.aged) },
|
||||
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_META_NAMESPACE = "meta"
|
||||
const val EH_UPLOADER_NAMESPACE = "uploader"
|
||||
const val EH_VISIBILITY_NAMESPACE = "visibility"
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
|
||||
@@ -41,10 +41,9 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
title?.let { getString(R.string.title) to it },
|
||||
path.nullIfEmpty()?.joinToString("/", prefix = "/")
|
||||
?.let { getString(R.string.path) to it },
|
||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(path.nullIfEmpty(), { it.joinToString("/", prefix = "/") }) { getString(R.string.path) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
hbId?.let { getString(R.string.id) to it.toString() },
|
||||
hbUrl?.let { getString(R.string.url) to it },
|
||||
thumbnail?.let { getString(R.string.thumbnail_url) to it },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
length?.let { getString(R.string.page_count) to it.toString() },
|
||||
getItem(hbId) { getString(R.string.id) },
|
||||
getItem(hbUrl) { getString(R.string.url) },
|
||||
getItem(thumbnail) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
hlId?.let { getString(R.string.id) to it },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
||||
artists.nullIfEmpty()?.joinToString()?.let { getString(R.string.artist) to it },
|
||||
genre?.let { getString(R.string.genre) to it },
|
||||
language?.let { getString(R.string.language) to it },
|
||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
||||
getItem(hlId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(artists.nullIfEmpty(), { it.joinToString() }) { getString(R.string.artist) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(language) { getString(R.string.language) },
|
||||
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>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
mdUuid?.let { getString(R.string.id) to it },
|
||||
// mdUrl?.let { getString(R.string.url) to it },
|
||||
cover?.let { getString(R.string.thumbnail_url) to it },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
authors?.let { getString(R.string.author) to it.joinToString() },
|
||||
artists?.let { getString(R.string.artist) to it.joinToString() },
|
||||
langFlag?.let { getString(R.string.language) to it },
|
||||
lastChapterNumber?.let { getString(R.string.last_chapter_number) to it.toString() },
|
||||
rating?.let { getString(R.string.average_rating) to it.toString() },
|
||||
// users?.let { getString(R.string.total_ratings) to it },
|
||||
status?.let { getString(R.string.status) to it.toString() },
|
||||
// missing_chapters?.let { getString(R.string.missing_chapters) to it },
|
||||
followStatus?.let { getString(R.string.follow_status) to it.toString() },
|
||||
anilistId?.let { getString(R.string.anilist_id) to it },
|
||||
kitsuId?.let { getString(R.string.kitsu_id) to it },
|
||||
myAnimeListId?.let { getString(R.string.mal_id) to it },
|
||||
mangaUpdatesId?.let { getString(R.string.manga_updates_id) to it },
|
||||
animePlanetId?.let { getString(R.string.anime_planet_id) to it },
|
||||
getItem(mdUuid) { getString(R.string.id) },
|
||||
// getItem(mdUrl) { getString(R.string.url) },
|
||||
getItem(cover) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(authors, { it.joinToString() }) { getString(R.string.author) },
|
||||
getItem(artists, { it.joinToString() }) { getString(R.string.artist) },
|
||||
getItem(langFlag) { getString(R.string.language) },
|
||||
getItem(lastChapterNumber) { getString(R.string.last_chapter_number) },
|
||||
getItem(rating) { getString(R.string.average_rating) },
|
||||
// getItem(users) { getString(R.string.total_ratings) },
|
||||
getItem(status) { getString(R.string.status) },
|
||||
// getItem(missing_chapters) { getString(R.string.missing_chapters) },
|
||||
getItem(followStatus) { getString(R.string.follow_status) },
|
||||
getItem(anilistId) { getString(R.string.anilist_id) },
|
||||
getItem(kitsuId) { getString(R.string.kitsu_id) },
|
||||
getItem(myAnimeListId) { getString(R.string.mal_id) },
|
||||
getItem(mangaUpdatesId) { getString(R.string.manga_updates_id) },
|
||||
getItem(animePlanetId) { getString(R.string.anime_planet_id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,17 +88,17 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
nhId?.let { getString(R.string.id) to it.toString() },
|
||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) },
|
||||
favoritesCount?.let { getString(R.string.total_favorites) to it.toString() },
|
||||
mediaId?.let { getString(R.string.media_id) to it },
|
||||
japaneseTitle?.let { getString(R.string.japanese_title) to it },
|
||||
englishTitle?.let { getString(R.string.english_title) to it },
|
||||
shortTitle?.let { getString(R.string.short_title) to it },
|
||||
coverImageType?.let { getString(R.string.cover_image_file_type) to it },
|
||||
pageImageTypes.size.let { getString(R.string.page_count) to it.toString() },
|
||||
thumbnailImageType?.let { getString(R.string.thumbnail_image_file_type) to it },
|
||||
scanlator?.let { getString(R.string.scanlator) to it },
|
||||
getItem(nhId) { getString(R.string.id) },
|
||||
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) }) { getString(R.string.date_posted) },
|
||||
getItem(favoritesCount) { getString(R.string.total_favorites) },
|
||||
getItem(mediaId) { getString(R.string.media_id) },
|
||||
getItem(japaneseTitle) { getString(R.string.japanese_title) },
|
||||
getItem(englishTitle) { getString(R.string.english_title) },
|
||||
getItem(shortTitle) { getString(R.string.short_title) },
|
||||
getItem(coverImageType) { getString(R.string.cover_image_file_type) },
|
||||
getItem(pageImageTypes.size) { getString(R.string.page_count) },
|
||||
getItem(thumbnailImageType) { getString(R.string.thumbnail_image_file_type) },
|
||||
getItem(scanlator) { getString(R.string.scanlator) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,17 +67,16 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
pvId?.let { getString(R.string.id) to it },
|
||||
url?.let { getString(R.string.url) to it },
|
||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
altTitles.nullIfEmpty()?.joinToString()
|
||||
?.let { getString(R.string.alt_titles) to it },
|
||||
artist?.let { getString(R.string.artist) to it },
|
||||
genre?.let { getString(R.string.genre) to it },
|
||||
rating?.let { getString(R.string.average_rating) to it.toString() },
|
||||
status?.let { getString(R.string.status) to it },
|
||||
lang?.let { getString(R.string.language) to it },
|
||||
getItem(pvId) { getString(R.string.id) },
|
||||
getItem(url) { getString(R.string.url) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitles.nullIfEmpty(), { it.joinToString() }) { getString(R.string.alt_titles) },
|
||||
getItem(artist) { getString(R.string.artist) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(rating) { getString(R.string.average_rating) },
|
||||
getItem(status) { getString(R.string.status) },
|
||||
getItem(lang) { getString(R.string.language) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +56,16 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
prId?.let { getString(R.string.id) to it.toString() },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
altTitle?.let { getString(R.string.alt_title) to it },
|
||||
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
|
||||
uploaderDisp?.let { getString(R.string.uploader_capital) to it },
|
||||
uploader?.let { getString(R.string.uploader) to it },
|
||||
pages?.let { getString(R.string.page_count) to it.toString() },
|
||||
fileSize?.let { getString(R.string.gallery_size) to it },
|
||||
ratingCount?.let { getString(R.string.total_ratings) to it.toString() },
|
||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
||||
getItem(prId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitle) { getString(R.string.alt_title) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(uploaderDisp) { getString(R.string.uploader_capital) },
|
||||
getItem(uploader) { getString(R.string.uploader) },
|
||||
getItem(pages) { getString(R.string.page_count) },
|
||||
getItem(fileSize) { getString(R.string.gallery_size) },
|
||||
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,20 +69,20 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
tmId?.let { getString(R.string.id) to it.toString() },
|
||||
title?.let { getString(R.string.title) to it },
|
||||
uploader?.let { getString(R.string.uploader) to it },
|
||||
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) },
|
||||
length?.let { getString(R.string.page_count) to it.toString() },
|
||||
ratingString?.let { getString(R.string.rating_string) to it },
|
||||
averageRating?.let { getString(R.string.average_rating) to it.toString() },
|
||||
userRatings?.let { getString(R.string.total_ratings) to it.toString() },
|
||||
favorites?.let { getString(R.string.total_favorites) to it.toString() },
|
||||
category?.let { getString(R.string.genre) to it },
|
||||
collection?.let { getString(R.string.collection) to it },
|
||||
group?.let { getString(R.string.group) to it },
|
||||
parody.nullIfEmpty()?.joinToString()?.let { getString(R.string.parodies) to it },
|
||||
character.nullIfEmpty()?.joinToString()?.let { getString(R.string.characters) to it },
|
||||
getItem(tmId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(uploader) { getString(R.string.uploader) },
|
||||
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
getItem(ratingString) { getString(R.string.rating_string) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
getItem(userRatings) { getString(R.string.total_ratings) },
|
||||
getItem(favorites) { getString(R.string.total_favorites) },
|
||||
getItem(category) { getString(R.string.genre) },
|
||||
getItem(collection) { getString(R.string.collection) },
|
||||
getItem(group) { getString(R.string.group) },
|
||||
getItem(parody.nullIfEmpty(), { it.joinToString() }) { getString(R.string.parodies) },
|
||||
getItem(character.nullIfEmpty(), { it.joinToString() }) { getString(R.string.characters) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,15 @@ abstract class RaisedSearchMetadata {
|
||||
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) {
|
||||
val infoManga = createMangaInfo(manga.toMangaInfo()).toSManga()
|
||||
manga.copyFrom(infoManga)
|
||||
|
||||
@@ -20,8 +20,8 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
|
||||
|
||||
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
||||
// 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()
|
||||
else (EH_INTERCEPTORS[sourceId].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()
|
||||
}
|
||||
|
||||
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"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:drawable="@drawable/ic_tachi"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
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"
|
||||
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:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
@@ -35,7 +35,7 @@
|
||||
android:background="?attr/colorTertiary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
android:background="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/side_nav"
|
||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
|
||||
tools:visibility="visible">
|
||||
|
||||
@@ -73,11 +73,10 @@
|
||||
<com.google.android.material.navigationrail.NavigationRailView
|
||||
android:id="@+id/side_nav"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
app:elevation="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="?attr/actionBarSize"
|
||||
app:elevation="1dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/incognito_mode"
|
||||
app:menu="@menu/main_nav" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
|
||||
@@ -31,57 +31,63 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/migration_data_scrollView"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/migration_data_group"
|
||||
android:layout_width="match_parent"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
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_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/chapters" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/mig_chapters"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:checked="true"
|
||||
android:text="@string/chapters" />
|
||||
<CheckBox
|
||||
android:id="@+id/mig_categories"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/categories" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/mig_categories"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:checked="true"
|
||||
android:text="@string/categories" />
|
||||
<CheckBox
|
||||
android:id="@+id/mig_tracking"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/track" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/mig_tracking"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:checked="true"
|
||||
android:text="@string/track" />
|
||||
<CheckBox
|
||||
android:id="@+id/mig_custom_cover"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/custom_cover" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/mig_extra"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:checked="true"
|
||||
android:text="@string/log_extra" />
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
<CheckBox
|
||||
android:id="@+id/mig_extra"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/log_extra" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/migration_data_divider"
|
||||
@@ -91,17 +97,18 @@
|
||||
android:background="?android:attr/divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/migration_data_scrollView"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/migration_data_group"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/options_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/action_settings"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
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" />
|
||||
|
||||
<RadioGroup
|
||||
|
||||
@@ -350,9 +350,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/left_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="32dp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
tools:text="1" />
|
||||
@@ -371,9 +372,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/right_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="32dp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
tools:text="15" />
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
android:id="@+id/upper_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
tools:text="Top" />
|
||||
|
||||
@@ -19,7 +18,7 @@
|
||||
android:id="@+id/warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
@@ -44,6 +43,7 @@
|
||||
android:id="@+id/lower_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
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