mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Merge branch 'dev' into dev-settings-search
This commit is contained in:
		| @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import java.security.Security | ||||
| import org.acra.ACRA | ||||
| import org.acra.annotation.AcraCore | ||||
| import org.acra.annotation.AcraHttpSender | ||||
| @@ -24,6 +23,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.InjektScope | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import uy.kohesive.injekt.registry.default.DefaultRegistrar | ||||
| import java.security.Security | ||||
|  | ||||
| @AcraCore( | ||||
|     buildConfigClass = BuildConfig::class, | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| package eu.kanade.tachiyomi | ||||
|  | ||||
| import androidx.core.content.edit | ||||
| import androidx.preference.PreferenceManager | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceKeys | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.updater.UpdaterJob | ||||
| import eu.kanade.tachiyomi.extension.ExtensionUpdateJob | ||||
| import eu.kanade.tachiyomi.ui.library.LibrarySort | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView | ||||
| import java.io.File | ||||
|  | ||||
| object Migrations { | ||||
| @@ -89,6 +93,25 @@ object Migrations { | ||||
|                     preferences.librarySortingMode().set(LibrarySort.ALPHA) | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 52) { | ||||
|                 // Migrate library filters to tri-state versions | ||||
|                 val prefs = PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                 fun convertBooleanPrefToTriState(key: String): Int { | ||||
|                     val oldPrefValue = prefs.getBoolean(key, false) | ||||
|                     return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.STATE_INCLUDE | ||||
|                     else ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE | ||||
|                 } | ||||
|                 prefs.edit { | ||||
|                     putInt(PreferenceKeys.filterDownloaded, convertBooleanPrefToTriState("pref_filter_downloaded_key")) | ||||
|                     remove("pref_filter_downloaded_key") | ||||
|  | ||||
|                     putInt(PreferenceKeys.filterUnread, convertBooleanPrefToTriState("pref_filter_unread_key")) | ||||
|                     remove("pref_filter_unread_key") | ||||
|  | ||||
|                     putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key")) | ||||
|                     remove("pref_filter_completed_key") | ||||
|                 } | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|   | ||||
| @@ -8,9 +8,9 @@ import androidx.work.WorkManager | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     Worker(context, workerParams) { | ||||
| @@ -36,8 +36,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet | ||||
|             val interval = prefInterval ?: preferences.backupInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                     interval.toLong(), | ||||
|                     TimeUnit.HOURS, | ||||
|                     10, | ||||
|                     TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .build() | ||||
|   | ||||
| @@ -50,10 +50,10 @@ import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import kotlin.math.max | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import kotlin.math.max | ||||
|  | ||||
| class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|  | ||||
|   | ||||
| @@ -11,9 +11,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| internal class BackupNotifier(private val context: Context) { | ||||
|  | ||||
|   | ||||
| @@ -32,12 +32,9 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.chapter.NoChaptersException | ||||
| import eu.kanade.tachiyomi.util.system.acquireWakeLock | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import java.io.File | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| import kotlinx.coroutines.CoroutineExceptionHandler | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.Job | ||||
| @@ -45,6 +42,10 @@ import kotlinx.coroutines.launch | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
|  | ||||
| /** | ||||
|  * Restores backup from a JSON file. | ||||
| @@ -398,7 +399,12 @@ class BackupRestoreService : Service() { | ||||
|         return backupManager.restoreChapterFetchObservable(source, manga, chapters) | ||||
|             // If there's any error, return empty update and continue. | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 val errorMessage = if (it is NoChaptersException) { | ||||
|                     getString(R.string.no_chapters_error) | ||||
|                 } else { | ||||
|                     it.message | ||||
|                 } | ||||
|                 errors.add(Date() to "${manga.title} - $errorMessage") | ||||
|                 Pair(emptyList(), emptyList()) | ||||
|             } | ||||
|     } | ||||
|   | ||||
| @@ -9,13 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
| import okhttp3.Response | ||||
| import okio.buffer | ||||
| import okio.sink | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
|  | ||||
| /** | ||||
|  * Class used to create chapter cache | ||||
|   | ||||
| @@ -83,6 +83,11 @@ interface MangaQueries : DbProvider { | ||||
|         .withPutResolver(MangaFlagsPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateFlags(mangas: List<Manga>) = db.put() | ||||
|         .objects(mangas) | ||||
|         .withPutResolver(MangaFlagsPutResolver(true)) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateLastUpdated(manga: Manga) = db.put() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable | ||||
|  | ||||
| class MangaFlagsPutResolver : PutResolver<Manga>() { | ||||
| class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() { | ||||
|  | ||||
|     override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { | ||||
|         val updateQuery = mapToUpdateQuery(manga) | ||||
| @@ -19,11 +19,21 @@ class MangaFlagsPutResolver : PutResolver<Manga>() { | ||||
|         PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|     fun mapToUpdateQuery(manga: Manga): UpdateQuery { | ||||
|         val builder = UpdateQuery.builder() | ||||
|  | ||||
|         return if (updateAll) { | ||||
|             builder | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         } else { | ||||
|             builder | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) | ||||
|   | ||||
| @@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * Cache where we dump the downloads directory from the filesystem. This class is needed because | ||||
|   | ||||
| @@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.lang.chop | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import java.util.regex.Pattern | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.util.regex.Pattern | ||||
|  | ||||
| /** | ||||
|  * DownloadNotifier is used to show notifications when downloading one or multiple chapters. | ||||
| @@ -107,7 +107,9 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|             } | ||||
|  | ||||
|             val downloadingProgressText = context.getString( | ||||
|                 R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size | ||||
|                 R.string.chapter_downloading_progress, | ||||
|                 download.downloadedImages, | ||||
|                 download.pages!!.size | ||||
|             ) | ||||
|  | ||||
|             if (preferences.hideNotificationContent()) { | ||||
|   | ||||
| @@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.util.lang.plusAssign | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import java.io.File | ||||
| import kotlinx.coroutines.async | ||||
| import okhttp3.Response | ||||
| import rx.Observable | ||||
| @@ -31,6 +30,7 @@ import rx.schedulers.Schedulers | ||||
| import rx.subscriptions.CompositeSubscription | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
|  | ||||
| /** | ||||
|  * This class is the one in charge of downloading chapters. | ||||
|   | ||||
| @@ -5,9 +5,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadStore | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import java.util.concurrent.CopyOnWriteArrayList | ||||
| import rx.Observable | ||||
| import rx.subjects.PublishSubject | ||||
| import java.util.concurrent.CopyOnWriteArrayList | ||||
|  | ||||
| class DownloadQueue( | ||||
|     private val store: DownloadStore, | ||||
|   | ||||
| @@ -5,12 +5,12 @@ import android.util.Log | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.DataSource | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| import java.io.FileInputStream | ||||
| import java.io.FileNotFoundException | ||||
| import java.io.IOException | ||||
| import java.io.InputStream | ||||
| import timber.log.Timber | ||||
|  | ||||
| open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> { | ||||
|  | ||||
|   | ||||
| @@ -14,10 +14,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.util.isLocal | ||||
| import java.io.InputStream | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.InputStream | ||||
|  | ||||
| /** | ||||
|  * A class for loading a cover associated with a [Manga] that can be present in our own cache. | ||||
|   | ||||
| @@ -14,9 +14,9 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions | ||||
| import com.bumptech.glide.module.AppGlideModule | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import java.io.InputStream | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.InputStream | ||||
|  | ||||
| /** | ||||
|  * Class used to update Glide module settings | ||||
|   | ||||
| @@ -9,9 +9,9 @@ import androidx.work.WorkManager | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     Worker(context, workerParams) { | ||||
| @@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|                     .build() | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                     interval.toLong(), | ||||
|                     TimeUnit.HOURS, | ||||
|                     10, | ||||
|                     TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|   | ||||
| @@ -22,9 +22,9 @@ import eu.kanade.tachiyomi.util.lang.chop | ||||
| import eu.kanade.tachiyomi.util.system.notification | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class LibraryUpdateNotifier(private val context: Context) { | ||||
|  | ||||
| @@ -198,18 +198,23 @@ class LibraryUpdateNotifier(private val context: Context) { | ||||
|  | ||||
|             // Mark chapters as read action | ||||
|             addAction( | ||||
|                 R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read), | ||||
|                 R.drawable.ic_glasses_black_24dp, | ||||
|                 context.getString(R.string.action_mark_as_read), | ||||
|                 NotificationReceiver.markAsReadPendingBroadcast( | ||||
|                     context, | ||||
|                     manga, chapters, Notifications.ID_NEW_CHAPTERS | ||||
|                     manga, | ||||
|                     chapters, | ||||
|                     Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|             // View chapters action | ||||
|             addAction( | ||||
|                 R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters), | ||||
|                 R.drawable.ic_book_24dp, | ||||
|                 context.getString(R.string.action_view_chapters), | ||||
|                 NotificationReceiver.openChapterPendingActivity( | ||||
|                     context, | ||||
|                     manga, Notifications.ID_NEW_CHAPTERS | ||||
|                     manga, | ||||
|                     Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.content.Intent | ||||
| import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| 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.Category | ||||
| @@ -21,20 +22,21 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.util.chapter.NoChaptersException | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import eu.kanade.tachiyomi.util.prepUpdateCover | ||||
| import eu.kanade.tachiyomi.util.shouldDownloadNewChapters | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.system.acquireWakeLock | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import java.io.File | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
|  | ||||
| /** | ||||
|  * This class will take care of updating the chapters of the manga from the library. It can be | ||||
| @@ -268,7 +270,12 @@ class LibraryUpdateService( | ||||
|                 updateManga(manga) | ||||
|                     // If there's any error, return empty update and continue. | ||||
|                     .onErrorReturn { | ||||
|                         failedUpdates.add(Pair(manga, it.message)) | ||||
|                         val errorMessage = if (it is NoChaptersException) { | ||||
|                             getString(R.string.no_chapters_error) | ||||
|                         } else { | ||||
|                             it.message | ||||
|                         } | ||||
|                         failedUpdates.add(Pair(manga, errorMessage)) | ||||
|                         Pair(emptyList(), emptyList()) | ||||
|                     } | ||||
|                     // Filter out mangas without new chapters (or failed). | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.os.Handler | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreService | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| @@ -26,10 +25,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import java.io.File | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
|  | ||||
| /** | ||||
|  * Global [BroadcastReceiver] that runs on UI thread | ||||
| @@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             // Launch share activity and dismiss notification | ||||
|             ACTION_SHARE_IMAGE -> | ||||
|                 shareImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     context, | ||||
|                     intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Delete image from path and dismiss notification | ||||
|             ACTION_DELETE_IMAGE -> | ||||
|                 deleteImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     context, | ||||
|                     intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Share backup file | ||||
|             ACTION_SHARE_BACKUP -> | ||||
|                 shareBackup( | ||||
|                     context, intent.getParcelableExtra(EXTRA_URI), | ||||
|                     context, | ||||
|                     intent.getParcelableExtra(EXTRA_URI), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore( | ||||
| @@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             // Open reader activity | ||||
|             ACTION_OPEN_CHAPTER -> { | ||||
|                 openChapter( | ||||
|                     context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                     context, | ||||
|                     intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                     intent.getLongExtra(EXTRA_CHAPTER_ID, -1) | ||||
|                 ) | ||||
|             } | ||||
|   | ||||
| @@ -82,53 +82,62 @@ object Notifications { | ||||
|  | ||||
|         listOf( | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                 CHANNEL_COMMON, | ||||
|                 context.getString(R.string.channel_common), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                 CHANNEL_LIBRARY, | ||||
|                 context.getString(R.string.channel_library), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress), | ||||
|                 CHANNEL_DOWNLOADER_PROGRESS, | ||||
|                 context.getString(R.string.channel_progress), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 group = GROUP_DOWNLOADER | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete), | ||||
|                 CHANNEL_DOWNLOADER_COMPLETE, | ||||
|                 context.getString(R.string.channel_complete), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 group = GROUP_DOWNLOADER | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors), | ||||
|                 CHANNEL_DOWNLOADER_ERROR, | ||||
|                 context.getString(R.string.channel_errors), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 group = GROUP_DOWNLOADER | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), | ||||
|                 CHANNEL_NEW_CHAPTERS, | ||||
|                 context.getString(R.string.channel_new_chapters), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), | ||||
|                 CHANNEL_UPDATES_TO_EXTS, | ||||
|                 context.getString(R.string.channel_ext_updates), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress), | ||||
|                 CHANNEL_BACKUP_RESTORE_PROGRESS, | ||||
|                 context.getString(R.string.channel_progress), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 group = GROUP_BACKUP_RESTORE | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete), | ||||
|                 CHANNEL_BACKUP_RESTORE_COMPLETE, | ||||
|                 context.getString(R.string.channel_complete), | ||||
|                 NotificationManager.IMPORTANCE_HIGH | ||||
|             ).apply { | ||||
|                 group = GROUP_BACKUP_RESTORE | ||||
|   | ||||
| @@ -109,11 +109,11 @@ object PreferenceKeys { | ||||
|  | ||||
|     const val downloadedOnly = "pref_downloaded_only" | ||||
|  | ||||
|     const val filterDownloaded = "pref_filter_downloaded_key" | ||||
|     const val filterDownloaded = "pref_filter_library_downloaded" | ||||
|  | ||||
|     const val filterUnread = "pref_filter_unread_key" | ||||
|     const val filterUnread = "pref_filter_library_unread" | ||||
|  | ||||
|     const val filterCompleted = "pref_filter_completed_key" | ||||
|     const val filterCompleted = "pref_filter_library_completed" | ||||
|  | ||||
|     const val librarySortingMode = "library_sorting_mode" | ||||
|  | ||||
| @@ -165,6 +165,18 @@ object PreferenceKeys { | ||||
|  | ||||
|     const val enableDoh = "enable_doh" | ||||
|  | ||||
|     const val defaultChapterFilterByRead = "default_chapter_filter_by_read" | ||||
|  | ||||
|     const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded" | ||||
|  | ||||
|     const val defaultChapterFilterByBookmarked = "default_chapter_filter_by_bookmarked" | ||||
|  | ||||
|     const val defaultChapterSortBySourceOrNumber = "default_chapter_sort_by_source_or_number" // and upload date | ||||
|  | ||||
|     const val defaultChapterSortByAscendingOrDescending = "default_chapter_sort_by_ascending_or_descending" | ||||
|  | ||||
|     const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number" | ||||
|  | ||||
|     fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" | ||||
|  | ||||
|     fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" | ||||
|   | ||||
| @@ -8,19 +8,21 @@ import androidx.preference.PreferenceManager | ||||
| import com.tfcporciuncula.flow.FlowSharedPreferences | ||||
| import com.tfcporciuncula.flow.Preference | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.anilist.Anilist | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView | ||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import java.io.File | ||||
| import java.text.DateFormat | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values | ||||
|  | ||||
| @OptIn(ExperimentalCoroutinesApi::class) | ||||
| fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> { | ||||
| @@ -209,11 +211,11 @@ class PreferencesHelper(val context: Context) { | ||||
|  | ||||
|     fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true) | ||||
|  | ||||
|     fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false) | ||||
|     fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) | ||||
|  | ||||
|     fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false) | ||||
|     fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) | ||||
|  | ||||
|     fun filterCompleted() = flowPrefs.getBoolean(Keys.filterCompleted, false) | ||||
|     fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) | ||||
|  | ||||
|     fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0) | ||||
|  | ||||
| @@ -254,4 +256,27 @@ class PreferencesHelper(val context: Context) { | ||||
|     fun lastSearchQuerySearchSettings() = prefs.getString("last_search_query", "") | ||||
|      | ||||
|     fun lastSearchQuerySearchSettings(query: String) = prefs.edit { putString("last_search_query", query) } | ||||
|  | ||||
|     fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL) | ||||
|  | ||||
|     fun filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL) | ||||
|  | ||||
|     fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL) | ||||
|  | ||||
|     fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE) | ||||
|  | ||||
|     fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME) | ||||
|  | ||||
|     fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC) | ||||
|  | ||||
|     fun setChapterSettingsDefault(manga: Manga) { | ||||
|         prefs.edit { | ||||
|             putInt(Keys.defaultChapterFilterByRead, manga.readFilter) | ||||
|             putInt(Keys.defaultChapterFilterByDownloaded, manga.downloadedFilter) | ||||
|             putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter) | ||||
|             putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting) | ||||
|             putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode) | ||||
|             putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,12 +13,12 @@ import com.google.gson.JsonParser | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import eu.kanade.tachiyomi.network.asObservableSuccess | ||||
| import java.util.Calendar | ||||
| import okhttp3.MediaType.Companion.toMediaTypeOrNull | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import rx.Observable | ||||
| import java.util.Calendar | ||||
|  | ||||
| class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|  | ||||
| @@ -271,9 +271,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|         } | ||||
|  | ||||
|         return ALManga( | ||||
|             struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, | ||||
|             struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(), | ||||
|             date, struct["chapters"].nullInt ?: 0 | ||||
|             struct["id"].asInt, | ||||
|             struct["title"]["romaji"].asString, | ||||
|             struct["coverImage"]["large"].asString, | ||||
|             struct["description"].nullString.orEmpty(), | ||||
|             struct["type"].asString, | ||||
|             struct["status"].nullString.orEmpty(), | ||||
|             date, | ||||
|             struct["chapters"].nullInt ?: 0 | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| data class ALManga( | ||||
|     val media_id: Int, | ||||
|   | ||||
| @@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import eu.kanade.tachiyomi.network.POST | ||||
| import eu.kanade.tachiyomi.network.asObservableSuccess | ||||
| import java.net.URLEncoder | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.net.URLEncoder | ||||
|  | ||||
| class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) { | ||||
|  | ||||
|   | ||||
| @@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import java.text.DecimalFormat | ||||
| import rx.Completable | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DecimalFormat | ||||
|  | ||||
| class Kitsu(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|   | ||||
| @@ -11,13 +11,6 @@ import eu.kanade.tachiyomi.network.asObservableSuccess | ||||
| import eu.kanade.tachiyomi.util.lang.toCalendar | ||||
| import eu.kanade.tachiyomi.util.selectInt | ||||
| import eu.kanade.tachiyomi.util.selectText | ||||
| import java.io.BufferedReader | ||||
| import java.io.InputStreamReader | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Calendar | ||||
| import java.util.GregorianCalendar | ||||
| import java.util.Locale | ||||
| import java.util.zip.GZIPInputStream | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.MediaType.Companion.toMediaTypeOrNull | ||||
| import okhttp3.OkHttpClient | ||||
| @@ -30,6 +23,13 @@ import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import org.jsoup.parser.Parser | ||||
| import rx.Observable | ||||
| import java.io.BufferedReader | ||||
| import java.io.InputStreamReader | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Calendar | ||||
| import java.util.GregorianCalendar | ||||
| import java.util.Locale | ||||
| import java.util.zip.GZIPInputStream | ||||
|  | ||||
| class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { | ||||
|  | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.updater | ||||
|  | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker | ||||
| import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker | ||||
|  | ||||
| abstract class UpdateChecker { | ||||
|  | ||||
|     companion object { | ||||
|         fun getUpdateChecker(): UpdateChecker { | ||||
|             return if (BuildConfig.DEBUG) { | ||||
|                 DevRepoUpdateChecker() | ||||
|             } else { | ||||
|                 GithubUpdateChecker() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns observable containing release information | ||||
|      */ | ||||
|     abstract suspend fun checkForUpdate(): UpdateResult | ||||
| } | ||||
| @@ -13,9 +13,10 @@ import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     Worker(context, workerParams) { | ||||
| @@ -23,7 +24,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     override fun doWork(): Result { | ||||
|         return runBlocking { | ||||
|             try { | ||||
|                 val result = UpdateChecker.getUpdateChecker().checkForUpdate() | ||||
|                 val result = GithubUpdateChecker().checkForUpdate() | ||||
|  | ||||
|                 if (result is UpdateResult.NewUpdate<*>) { | ||||
|                     val url = result.release.downloadLink | ||||
| @@ -65,8 +66,10 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|                 .build() | ||||
|  | ||||
|             val request = PeriodicWorkRequestBuilder<UpdaterJob>( | ||||
|                 3, TimeUnit.DAYS, | ||||
|                 3, TimeUnit.HOURS | ||||
|                 3, | ||||
|                 TimeUnit.DAYS, | ||||
|                 3, | ||||
|                 TimeUnit.HOURS | ||||
|             ) | ||||
|                 .addTag(TAG) | ||||
|                 .setConstraints(constraints) | ||||
|   | ||||
| @@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import eu.kanade.tachiyomi.util.system.acquireWakeLock | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import java.io.File | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
|  | ||||
| class UpdaterService : Service() { | ||||
|  | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.updater.devrepo | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.updater.Release | ||||
|  | ||||
| class DevRepoRelease(override val info: String) : Release { | ||||
|  | ||||
|     override val downloadLink: String | ||||
|         get() = LATEST_URL | ||||
|  | ||||
|     companion object { | ||||
|         const val LATEST_URL = "https://tachiyomi.kanade.eu/latest" | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.updater.devrepo | ||||
|  | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateChecker | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateResult | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.network.await | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.withContext | ||||
| import okhttp3.OkHttpClient | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class DevRepoUpdateChecker : UpdateChecker() { | ||||
|  | ||||
|     private val client: OkHttpClient by lazy { | ||||
|         Injekt.get<NetworkHelper>().client.newBuilder() | ||||
|             .followRedirects(false) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     private val versionRegex: Regex by lazy { | ||||
|         Regex("tachiyomi-r(\\d+).apk") | ||||
|     } | ||||
|  | ||||
|     override suspend fun checkForUpdate(): UpdateResult { | ||||
|         val response = withContext(Dispatchers.IO) { | ||||
|             client.newCall(GET(DevRepoRelease.LATEST_URL)).await() | ||||
|         } | ||||
|  | ||||
|         // Get latest repo version number from header in format "Location: tachiyomi-r1512.apk" | ||||
|         val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1] | ||||
|  | ||||
|         return if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) { | ||||
|             DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber")) | ||||
|         } else { | ||||
|             DevRepoUpdateResult.NoNewUpdate() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.updater.devrepo | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateResult | ||||
|  | ||||
| sealed class DevRepoUpdateResult : UpdateResult() { | ||||
|  | ||||
|     class NewUpdate(release: DevRepoRelease) : UpdateResult.NewUpdate<DevRepoRelease>(release) | ||||
|     class NoNewUpdate : UpdateResult.NoNewUpdate() | ||||
| } | ||||
| @@ -28,5 +28,5 @@ class GithubRelease( | ||||
|      * Assets class containing download url. | ||||
|      * @param downloadLink download url. | ||||
|      */ | ||||
|     inner class Assets(@SerializedName("browser_download_url") val downloadLink: String) | ||||
|     class Assets(@SerializedName("browser_download_url") val downloadLink: String) | ||||
| } | ||||
|   | ||||
| @@ -4,11 +4,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import retrofit2.Retrofit | ||||
| import retrofit2.converter.gson.GsonConverterFactory | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Path | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * Used to connect with the GitHub API. | ||||
|  * Used to connect with the GitHub API to get the latest release version from a repo. | ||||
|  */ | ||||
| interface GithubService { | ||||
|  | ||||
| @@ -24,6 +25,6 @@ interface GithubService { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @GET("/repos/inorichi/tachiyomi/releases/latest") | ||||
|     suspend fun getLatestVersion(): GithubRelease | ||||
|     @GET("/repos/{repo}/releases/latest") | ||||
|     suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,43 @@ | ||||
| package eu.kanade.tachiyomi.data.updater.github | ||||
|  | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateChecker | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateResult | ||||
|  | ||||
| class GithubUpdateChecker : UpdateChecker() { | ||||
| class GithubUpdateChecker { | ||||
|  | ||||
|     private val service: GithubService = GithubService.create() | ||||
|  | ||||
|     override suspend fun checkForUpdate(): UpdateResult { | ||||
|         val release = service.getLatestVersion() | ||||
|     private val repo: String by lazy { | ||||
|         if (BuildConfig.DEBUG) { | ||||
|             "tachiyomiorg/android-app-preview" | ||||
|         } else { | ||||
|             "inorichi/tachiyomi" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         val newVersion = release.version.replace("[^\\d.]".toRegex(), "") | ||||
|     suspend fun checkForUpdate(): UpdateResult { | ||||
|         val release = service.getLatestVersion(repo) | ||||
|  | ||||
|         // Check if latest version is different from current version | ||||
|         return if (newVersion != BuildConfig.VERSION_NAME) { | ||||
|         return if (isNewVersion(release.version)) { | ||||
|             GithubUpdateResult.NewUpdate(release) | ||||
|         } else { | ||||
|             GithubUpdateResult.NoNewUpdate() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun isNewVersion(versionTag: String): Boolean { | ||||
|         // Removes prefixes like "r" or "v" | ||||
|         val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") | ||||
|  | ||||
|         return if (BuildConfig.DEBUG) { | ||||
|             // Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo | ||||
|             // tagged as something like "r1234" | ||||
|             newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt() | ||||
|         } else { | ||||
|             // Release builds: based on releases in "inorichi/tachiyomi" repo | ||||
|             // tagged as something like "v0.1.2" | ||||
|             newVersion != BuildConfig.VERSION_NAME | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,10 +16,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi | ||||
| import eu.kanade.tachiyomi.util.system.notification | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.coroutineScope | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
| @@ -73,8 +73,10 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam | ||||
|                     .build() | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>( | ||||
|                     12, TimeUnit.HOURS, | ||||
|                     1, TimeUnit.HOURS | ||||
|                     12, | ||||
|                     TimeUnit.HOURS, | ||||
|                     1, | ||||
|                     TimeUnit.HOURS | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|   | ||||
| @@ -9,10 +9,10 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.LoadResult | ||||
| import eu.kanade.tachiyomi.extension.util.ExtensionLoader | ||||
| import java.util.Date | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.withContext | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.util.Date | ||||
|  | ||||
| internal class ExtensionGithubApi { | ||||
|  | ||||
|   | ||||
| @@ -13,11 +13,11 @@ import com.jakewharton.rxrelay.PublishRelay | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import java.io.File | ||||
| import java.util.concurrent.TimeUnit | ||||
| import rx.Observable | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * The installer which installs, updates and uninstalls the extensions. | ||||
|   | ||||
| @@ -178,7 +178,13 @@ internal object ExtensionLoader { | ||||
|         } | ||||
|  | ||||
|         val extension = Extension.Installed( | ||||
|             extName, pkgName, versionName, versionCode, lang, isNsfw, sources, | ||||
|             extName, | ||||
|             pkgName, | ||||
|             versionName, | ||||
|             versionCode, | ||||
|             lang, | ||||
|             isNsfw, | ||||
|             sources, | ||||
|             isUnofficial = signatureHash != officialSignature | ||||
|         ) | ||||
|         return LoadResult.Success(extension) | ||||
|   | ||||
| @@ -16,15 +16,15 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil | ||||
| import eu.kanade.tachiyomi.util.system.isOutdated | ||||
| import eu.kanade.tachiyomi.util.system.setDefaultSettings | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.CountDownLatch | ||||
| import java.util.concurrent.TimeUnit | ||||
| import okhttp3.Cookie | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.CountDownLatch | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class CloudflareInterceptor(private val context: Context) : Interceptor { | ||||
|  | ||||
|   | ||||
| @@ -3,15 +3,15 @@ package eu.kanade.tachiyomi.network | ||||
| import android.content.Context | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import java.io.File | ||||
| import java.net.InetAddress | ||||
| import java.util.concurrent.TimeUnit | ||||
| import okhttp3.Cache | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.dnsoverhttps.DnsOverHttps | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.net.InetAddress | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class NetworkHelper(context: Context) { | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.network | ||||
|  | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.atomic.AtomicBoolean | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||||
| import okhttp3.Call | ||||
| import okhttp3.Callback | ||||
| @@ -13,6 +9,10 @@ import okhttp3.Response | ||||
| import rx.Observable | ||||
| import rx.Producer | ||||
| import rx.Subscription | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.atomic.AtomicBoolean | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
|  | ||||
| fun Call.asObservable(): Observable<Response> { | ||||
|     return Observable.unsafeCreate { subscriber -> | ||||
| @@ -54,22 +54,24 @@ fun Call.asObservable(): Observable<Response> { | ||||
| // Based on https://github.com/gildor/kotlin-coroutines-okhttp | ||||
| suspend fun Call.await(assertSuccess: Boolean = false): Response { | ||||
|     return suspendCancellableCoroutine { continuation -> | ||||
|         enqueue(object : Callback { | ||||
|             override fun onResponse(call: Call, response: Response) { | ||||
|                 if (assertSuccess && !response.isSuccessful) { | ||||
|                     continuation.resumeWithException(Exception("HTTP error ${response.code}")) | ||||
|                     return | ||||
|         enqueue( | ||||
|             object : Callback { | ||||
|                 override fun onResponse(call: Call, response: Response) { | ||||
|                     if (assertSuccess && !response.isSuccessful) { | ||||
|                         continuation.resumeWithException(Exception("HTTP error ${response.code}")) | ||||
|                         return | ||||
|                     } | ||||
|  | ||||
|                     continuation.resume(response) | ||||
|                 } | ||||
|  | ||||
|                 continuation.resume(response) | ||||
|                 override fun onFailure(call: Call, e: IOException) { | ||||
|                     // Don't bother with resuming the continuation if it is already cancelled. | ||||
|                     if (continuation.isCancelled) return | ||||
|                     continuation.resumeWithException(e) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             override fun onFailure(call: Call, e: IOException) { | ||||
|                 // Don't bother with resuming the continuation if it is already cancelled. | ||||
|                 if (continuation.isCancelled) return | ||||
|                 continuation.resumeWithException(e) | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         continuation.invokeOnCancellation { | ||||
|             try { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.network | ||||
|  | ||||
| import java.io.IOException | ||||
| import okhttp3.MediaType | ||||
| import okhttp3.ResponseBody | ||||
| import okio.Buffer | ||||
| @@ -8,6 +7,7 @@ import okio.BufferedSource | ||||
| import okio.ForwardingSource | ||||
| import okio.Source | ||||
| import okio.buffer | ||||
| import java.io.IOException | ||||
|  | ||||
| class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| package eu.kanade.tachiyomi.network | ||||
|  | ||||
| import java.util.concurrent.TimeUnit.MINUTES | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.Headers | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody | ||||
| import java.util.concurrent.TimeUnit.MINUTES | ||||
|  | ||||
| private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() | ||||
| private val DEFAULT_HEADERS = Headers.Builder().build() | ||||
|   | ||||
| @@ -14,6 +14,10 @@ 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 junrar.Archive | ||||
| import junrar.rarfile.FileHeader | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| import java.io.FileInputStream | ||||
| import java.io.InputStream | ||||
| @@ -21,10 +25,6 @@ import java.util.Locale | ||||
| import java.util.concurrent.TimeUnit | ||||
| import java.util.zip.ZipEntry | ||||
| import java.util.zip.ZipFile | ||||
| import junrar.Archive | ||||
| import junrar.rarfile.FileHeader | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
|  | ||||
| class LocalSource(private val context: Context) : CatalogueSource { | ||||
|     companion object { | ||||
|   | ||||
| @@ -10,15 +10,15 @@ import eu.kanade.tachiyomi.source.model.MangasPage | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import java.net.URI | ||||
| import java.net.URISyntaxException | ||||
| import java.security.MessageDigest | ||||
| import okhttp3.Headers | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.net.URI | ||||
| import java.net.URISyntaxException | ||||
| import java.security.MessageDigest | ||||
|  | ||||
| /** | ||||
|  * A simple implementation for sources from a website. | ||||
|   | ||||
| @@ -7,11 +7,11 @@ import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import androidx.viewbinding.ViewBinding | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values | ||||
|  | ||||
| abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { | ||||
|  | ||||
|   | ||||
| @@ -22,27 +22,29 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : | ||||
|     lateinit var binding: VB | ||||
|  | ||||
|     init { | ||||
|         addLifecycleListener(object : LifecycleListener() { | ||||
|             override fun postCreateView(controller: Controller, view: View) { | ||||
|                 onViewCreated(view) | ||||
|             } | ||||
|         addLifecycleListener( | ||||
|             object : LifecycleListener() { | ||||
|                 override fun postCreateView(controller: Controller, view: View) { | ||||
|                     onViewCreated(view) | ||||
|                 } | ||||
|  | ||||
|             override fun preCreateView(controller: Controller) { | ||||
|                 Timber.d("Create view for ${controller.instance()}") | ||||
|             } | ||||
|                 override fun preCreateView(controller: Controller) { | ||||
|                     Timber.d("Create view for ${controller.instance()}") | ||||
|                 } | ||||
|  | ||||
|             override fun preAttach(controller: Controller, view: View) { | ||||
|                 Timber.d("Attach view for ${controller.instance()}") | ||||
|             } | ||||
|                 override fun preAttach(controller: Controller, view: View) { | ||||
|                     Timber.d("Attach view for ${controller.instance()}") | ||||
|                 } | ||||
|  | ||||
|             override fun preDetach(controller: Controller, view: View) { | ||||
|                 Timber.d("Detach view for ${controller.instance()}") | ||||
|             } | ||||
|                 override fun preDetach(controller: Controller, view: View) { | ||||
|                     Timber.d("Detach view for ${controller.instance()}") | ||||
|                 } | ||||
|  | ||||
|             override fun preDestroyView(controller: Controller, view: View) { | ||||
|                 Timber.d("Destroy view for ${controller.instance()}") | ||||
|                 override fun preDestroyView(controller: Controller, view: View) { | ||||
|                     Timber.d("Destroy view for ${controller.instance()}") | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override val containerView: View? | ||||
| @@ -98,17 +100,19 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : | ||||
|     var expandActionViewFromInteraction = false | ||||
|  | ||||
|     fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) { | ||||
|         setOnActionExpandListener(object : MenuItem.OnActionExpandListener { | ||||
|             override fun onMenuItemActionExpand(item: MenuItem): Boolean { | ||||
|                 return onExpand?.invoke(item) ?: true | ||||
|             } | ||||
|         setOnActionExpandListener( | ||||
|             object : MenuItem.OnActionExpandListener { | ||||
|                 override fun onMenuItemActionExpand(item: MenuItem): Boolean { | ||||
|                     return onExpand?.invoke(item) ?: true | ||||
|                 } | ||||
|  | ||||
|             override fun onMenuItemActionCollapse(item: MenuItem): Boolean { | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|                 override fun onMenuItemActionCollapse(item: MenuItem): Boolean { | ||||
|                     activity?.invalidateOptionsMenu() | ||||
|  | ||||
|                 return onCollapse?.invoke(item) ?: true | ||||
|                     return onCollapse?.invoke(item) ?: true | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         if (expandActionViewFromInteraction) { | ||||
|             expandActionViewFromInteraction = false | ||||
|   | ||||
| @@ -10,12 +10,12 @@ import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import java.util.concurrent.TimeUnit | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| private typealias ExtensionTuple = | ||||
|     Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>> | ||||
|   | ||||
| @@ -20,8 +20,6 @@ import androidx.preference.PreferenceManager | ||||
| import androidx.preference.PreferenceScreen | ||||
| import androidx.preference.SwitchPreferenceCompat | ||||
| import androidx.recyclerview.widget.ConcatAdapter | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore | ||||
| @@ -92,7 +90,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) : | ||||
|             ExtensionDetailsHeaderAdapter(presenter), | ||||
|             initPreferencesAdapter(context, extension) | ||||
|         ) | ||||
|         binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL)) | ||||
|     } | ||||
|  | ||||
|     private fun initPreferencesAdapter(context: Context, extension: Extension.Installed): PreferenceGroupAdapter { | ||||
| @@ -112,7 +109,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : | ||||
|                 .forEach { | ||||
|                     val preferenceBlock = { | ||||
|                         it.value | ||||
|                             .sortedWith(compareBy({ !it.isEnabled() }, { it.name })) | ||||
|                             .sortedWith(compareBy({ !it.isEnabled() }, { it.name.toLowerCase() })) | ||||
|                             .forEach { source -> | ||||
|                                 val sourcePrefs = mutableListOf<Preference>() | ||||
|  | ||||
|   | ||||
| @@ -19,8 +19,6 @@ import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceGroupAdapter | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.preference.PreferenceScreen | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore | ||||
| @@ -86,7 +84,6 @@ class SourcePreferencesController(bundle: Bundle? = null) : | ||||
|  | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(context) | ||||
|         binding.recycler.adapter = PreferenceGroupAdapter(screen) | ||||
|         binding.recycler.addItemDecoration(DividerItemDecoration(context, VERTICAL)) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|   | ||||
| @@ -13,10 +13,10 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import java.util.Date | ||||
| import rx.Observable | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import java.util.Date | ||||
|  | ||||
| class SearchPresenter( | ||||
|     initialQuery: String? = "", | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class MigrationSourcesPresenter( | ||||
|         val header = SelectionHeader() | ||||
|         return library.map { it.source }.toSet() | ||||
|             .mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null } | ||||
|             .sortedBy { it.name } | ||||
|             .sortedBy { it.name.toLowerCase() } | ||||
|             .map { SourceItem(it, header) } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,9 +16,9 @@ import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import java.util.TreeMap | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.TreeMap | ||||
|  | ||||
| class SourceFilterController : SettingsController() { | ||||
|  | ||||
| @@ -42,7 +42,7 @@ class SourceFilterController : SettingsController() { | ||||
|         ) | ||||
|  | ||||
|         orderedLangs.forEach { lang -> | ||||
|             val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name } | ||||
|             val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.toLowerCase() } | ||||
|  | ||||
|             // Create a preference group and set initial state and change listener | ||||
|             switchPreferenceCategory { | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import java.util.TreeMap | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.Job | ||||
| @@ -21,6 +20,7 @@ import rx.Observable | ||||
| import rx.Subscription | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.TreeMap | ||||
|  | ||||
| /** | ||||
|  * Presenter of [SourceController] | ||||
| @@ -128,7 +128,7 @@ class SourcePresenter( | ||||
|         return sourceManager.getCatalogueSources() | ||||
|             .filter { it.lang in languages } | ||||
|             .filterNot { it.id.toString() in disabledSourceIds } | ||||
|             .sortedBy { "(${it.lang}) ${it.name}" } + | ||||
|             .sortedBy { "(${it.lang}) ${it.name.toLowerCase()}" } + | ||||
|             sourceManager.get(LocalSource.ID) as LocalSource | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.widget.SearchView | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| @@ -216,7 +215,6 @@ open class BrowseSourceController(bundle: Bundle) : | ||||
|                 id = R.id.recycler | ||||
|                 layoutManager = LinearLayoutManager(context) | ||||
|                 layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) | ||||
|                 addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) | ||||
|             } | ||||
|         } else { | ||||
|             (binding.catalogueView.inflate(R.layout.source_recycler_autofit) as AutofitRecyclerView).apply { | ||||
|   | ||||
| @@ -28,9 +28,8 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TextItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem | ||||
| import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import java.util.Date | ||||
| import kotlinx.coroutines.flow.subscribe | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| @@ -39,6 +38,7 @@ import rx.subjects.PublishSubject | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
|  | ||||
| /** | ||||
|  * Presenter of [BrowseSourceController]. | ||||
| @@ -268,6 +268,8 @@ open class BrowseSourcePresenter( | ||||
|  | ||||
|         if (!manga.favorite) { | ||||
|             manga.removeCovers(coverCache) | ||||
|         } else { | ||||
|             ChapterSettingsHelper.applySettingDefaults(manga) | ||||
|         } | ||||
|  | ||||
|         db.insertManga(manga).executeAsBlocking() | ||||
|   | ||||
| @@ -38,10 +38,13 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo | ||||
|                 val coverHeight = parent.itemWidth / 3 * 4 | ||||
|                 view.apply { | ||||
|                     card.layoutParams = FrameLayout.LayoutParams( | ||||
|                         MATCH_PARENT, coverHeight | ||||
|                         MATCH_PARENT, | ||||
|                         coverHeight | ||||
|                     ) | ||||
|                     gradient.layoutParams = FrameLayout.LayoutParams( | ||||
|                         MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM | ||||
|                         MATCH_PARENT, | ||||
|                         coverHeight / 2, | ||||
|                         Gravity.BOTTOM | ||||
|                     ) | ||||
|                 } | ||||
|                 SourceGridHolder(view, adapter) | ||||
| @@ -51,7 +54,8 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo | ||||
|                 val coverHeight = parent.itemWidth / 3 * 4 | ||||
|                 view.apply { | ||||
|                     card.layoutParams = ConstraintLayout.LayoutParams( | ||||
|                         MATCH_PARENT, coverHeight | ||||
|                         MATCH_PARENT, | ||||
|                         coverHeight | ||||
|                     ) | ||||
|                 } | ||||
|                 SourceComfortableGridHolder(view, adapter) | ||||
|   | ||||
| @@ -30,7 +30,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec | ||||
|         spinner.prompt = filter.name | ||||
|         spinner.adapter = ArrayAdapter<Any>( | ||||
|             holder.itemView.context, | ||||
|             android.R.layout.simple_spinner_item, filter.values | ||||
|             android.R.layout.simple_spinner_item, | ||||
|             filter.values | ||||
|         ).apply { | ||||
|             setDropDownViewResource(R.layout.common_spinner_item) | ||||
|         } | ||||
|   | ||||
| @@ -9,10 +9,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R as TR | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import eu.kanade.tachiyomi.R as TR | ||||
|  | ||||
| open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriStateItem.Holder>() { | ||||
|  | ||||
|   | ||||
| @@ -108,17 +108,19 @@ open class GlobalSearchController( | ||||
|         val searchView = searchItem.actionView as SearchView | ||||
|         searchView.maxWidth = Int.MAX_VALUE | ||||
|  | ||||
|         searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { | ||||
|             override fun onMenuItemActionExpand(item: MenuItem?): Boolean { | ||||
|                 searchView.onActionViewExpanded() // Required to show the query in the view | ||||
|                 searchView.setQuery(presenter.query, false) | ||||
|                 return true | ||||
|             } | ||||
|         searchItem.setOnActionExpandListener( | ||||
|             object : MenuItem.OnActionExpandListener { | ||||
|                 override fun onMenuItemActionExpand(item: MenuItem?): Boolean { | ||||
|                     searchView.onActionViewExpanded() // Required to show the query in the view | ||||
|                     searchView.setQuery(presenter.query, false) | ||||
|                     return true | ||||
|                 } | ||||
|  | ||||
|             override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { | ||||
|                 return true | ||||
|                 override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { | ||||
|                     return true | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         searchView.queryTextEvents() | ||||
|             .filterIsInstance<QueryTextEvent.QuerySubmitted>() | ||||
|   | ||||
| @@ -108,7 +108,7 @@ open class GlobalSearchPresenter( | ||||
|         return sourceManager.getCatalogueSources() | ||||
|             .filter { it.lang in languages } | ||||
|             .filterNot { it.id.toString() in disabledSourceIds } | ||||
|             .sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name} (${it.lang})" })) | ||||
|             .sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name.toLowerCase()} (${it.lang})" })) | ||||
|     } | ||||
|  | ||||
|     private fun getSourcesToQuery(): List<CatalogueSource> { | ||||
| @@ -188,7 +188,7 @@ open class GlobalSearchPresenter( | ||||
|                             { it.results.isNullOrEmpty() }, | ||||
|                             // Same as initial sort, i.e. pinned first then alphabetically | ||||
|                             { it.source.id.toString() !in pinnedSourceIds }, | ||||
|                             { "${it.source.name} (${it.source.lang})" } | ||||
|                             { "${it.source.name.toLowerCase()} (${it.source.lang})" } | ||||
|                         ) | ||||
|                     ) | ||||
|             } | ||||
|   | ||||
| @@ -198,8 +198,11 @@ class CategoryController : | ||||
|             R.id.action_delete -> { | ||||
|                 undoHelper = UndoHelper(adapter, this) | ||||
|                 undoHelper?.start( | ||||
|                     adapter.selectedPositions, activity!!.root_coordinator, | ||||
|                     R.string.snack_categories_deleted, R.string.action_undo, 3000 | ||||
|                     adapter.selectedPositions, | ||||
|                     activity!!.root_coordinator, | ||||
|                     R.string.snack_categories_deleted, | ||||
|                     R.string.action_undo, | ||||
|                     3000 | ||||
|                 ) | ||||
|  | ||||
|                 mode.finish() | ||||
|   | ||||
| @@ -18,13 +18,13 @@ import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FabController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.util.view.shrinkOnScroll | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.android.view.clicks | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * Controller that shows the currently active downloads. | ||||
|   | ||||
| @@ -183,7 +183,7 @@ class LibraryController( | ||||
|             createActionModeIfNeeded() | ||||
|         } | ||||
|  | ||||
|         settingsSheet = LibrarySettingsSheet(activity!!) { group -> | ||||
|         settingsSheet = LibrarySettingsSheet(router) { group -> | ||||
|             when (group) { | ||||
|                 is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() | ||||
|                 is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged() | ||||
|   | ||||
| @@ -45,7 +45,9 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe | ||||
|                 view.apply { | ||||
|                     card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) | ||||
|                     gradient.layoutParams = FrameLayout.LayoutParams( | ||||
|                         MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM | ||||
|                         MATCH_PARENT, | ||||
|                         coverHeight / 2, | ||||
|                         Gravity.BOTTOM | ||||
|                     ) | ||||
|                 } | ||||
|                 LibraryCompactGridHolder(view, adapter) | ||||
| @@ -55,7 +57,8 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe | ||||
|                 val coverHeight = parent.itemWidth / 3 * 4 | ||||
|                 view.apply { | ||||
|                     card.layoutParams = ConstraintLayout.LayoutParams( | ||||
|                         MATCH_PARENT, coverHeight | ||||
|                         MATCH_PARENT, | ||||
|                         coverHeight | ||||
|                     ) | ||||
|                 } | ||||
|                 LibraryComfortableGridHolder(view, adapter) | ||||
|   | ||||
| @@ -19,14 +19,16 @@ import eu.kanade.tachiyomi.util.lang.combineLatest | ||||
| import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import java.util.Collections | ||||
| import java.util.Comparator | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Collections | ||||
| import java.util.Comparator | ||||
|  | ||||
| /** | ||||
|  * Class containing library information. | ||||
| @@ -110,34 +112,45 @@ class LibraryPresenter( | ||||
|      * @param map the map to filter. | ||||
|      */ | ||||
|     private fun applyFilters(map: LibraryMap): LibraryMap { | ||||
|         val filterDownloaded = preferences.downloadedOnly().get() || preferences.filterDownloaded().get() | ||||
|         val downloadedOnly = preferences.downloadedOnly().get() | ||||
|         val filterDownloaded = preferences.filterDownloaded().get() | ||||
|         val filterUnread = preferences.filterUnread().get() | ||||
|         val filterCompleted = preferences.filterCompleted().get() | ||||
|  | ||||
|         val filterFn: (LibraryItem) -> Boolean = f@{ item -> | ||||
|             // Filter when there isn't unread chapters. | ||||
|             if (filterUnread && item.manga.unread == 0) { | ||||
|                 return@f false | ||||
|         val filterFnUnread: (LibraryItem) -> Boolean = unread@{ item -> | ||||
|             if (filterUnread == STATE_IGNORE) return@unread true | ||||
|             val isUnread = item.manga.unread != 0 | ||||
|  | ||||
|             return@unread if (filterUnread == STATE_INCLUDE) isUnread | ||||
|             else !isUnread | ||||
|         } | ||||
|  | ||||
|         val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item -> | ||||
|             if (filterCompleted == STATE_IGNORE) return@completed true | ||||
|             val isCompleted = item.manga.status == SManga.COMPLETED | ||||
|  | ||||
|             return@completed if (filterCompleted == STATE_INCLUDE) isCompleted | ||||
|             else !isCompleted | ||||
|         } | ||||
|  | ||||
|         val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item -> | ||||
|             if (!downloadedOnly && filterDownloaded == STATE_IGNORE) return@downloaded true | ||||
|             val isDownloaded = when { | ||||
|                 item.manga.isLocal() -> true | ||||
|                 item.downloadCount != -1 -> item.downloadCount > 0 | ||||
|                 else -> downloadManager.getDownloadCount(item.manga) > 0 | ||||
|             } | ||||
|  | ||||
|             if (filterCompleted && item.manga.status != SManga.COMPLETED) { | ||||
|                 return@f false | ||||
|             } | ||||
|             return@downloaded if (downloadedOnly || filterDownloaded == STATE_INCLUDE) isDownloaded | ||||
|             else !isDownloaded | ||||
|         } | ||||
|  | ||||
|             // Filter when there are no downloads. | ||||
|             if (filterDownloaded) { | ||||
|                 // Local manga are always downloaded | ||||
|                 if (item.manga.isLocal()) { | ||||
|                     return@f true | ||||
|                 } | ||||
|                 // Don't bother with directory checking if download count has been set. | ||||
|                 if (item.downloadCount != -1) { | ||||
|                     return@f item.downloadCount > 0 | ||||
|                 } | ||||
|  | ||||
|                 return@f downloadManager.getDownloadCount(item.manga) > 0 | ||||
|             } | ||||
|             true | ||||
|         val filterFn: (LibraryItem) -> Boolean = filter@{ item -> | ||||
|             return@filter !( | ||||
|                 !filterFnUnread(item) || | ||||
|                     !filterFnCompleted(item) || | ||||
|                     !filterFnDownloaded(item) | ||||
|                 ) | ||||
|         } | ||||
|  | ||||
|         return map.mapValues { entry -> entry.value.filter(filterFn) } | ||||
|   | ||||
| @@ -1,33 +1,36 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_EXCLUDE | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE | ||||
| import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class LibrarySettingsSheet( | ||||
|     activity: Activity, | ||||
|     router: Router, | ||||
|     onGroupClickListener: (ExtendedNavigationView.Group) -> Unit | ||||
| ) : TabbedBottomSheetDialog(activity) { | ||||
| ) : TabbedBottomSheetDialog(router) { | ||||
|  | ||||
|     val filters: Filter | ||||
|     private val sort: Sort | ||||
|     private val display: Display | ||||
|  | ||||
|     init { | ||||
|         filters = Filter(activity) | ||||
|         filters = Filter(router.activity!!) | ||||
|         filters.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         sort = Sort(activity) | ||||
|         sort = Sort(router.activity!!) | ||||
|         sort.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         display = Display(activity) | ||||
|         display = Display(router.activity!!) | ||||
|         display.onGroupClicked = onGroupClickListener | ||||
|     } | ||||
|  | ||||
| @@ -59,33 +62,43 @@ class LibrarySettingsSheet( | ||||
|          * Returns true if there's at least one filter from [FilterGroup] active. | ||||
|          */ | ||||
|         fun hasActiveFilters(): Boolean { | ||||
|             return filterGroup.items.any { it.checked } | ||||
|             return filterGroup.items.any { it.state != Item.TriStateGroup.STATE_IGNORE } | ||||
|         } | ||||
|  | ||||
|         inner class FilterGroup : Group { | ||||
|  | ||||
|             private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) | ||||
|             private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) | ||||
|             private val completed = Item.CheckboxGroup(R.string.completed, this) | ||||
|             private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this) | ||||
|             private val unread = Item.TriStateGroup(R.string.action_filter_unread, this) | ||||
|             private val completed = Item.TriStateGroup(R.string.completed, this) | ||||
|  | ||||
|             override val header = null | ||||
|             override val items = listOf(downloaded, unread, completed) | ||||
|             override val footer = null | ||||
|  | ||||
|             override fun initModels() { | ||||
|                 downloaded.checked = preferences.downloadedOnly().get() || preferences.filterDownloaded().get() | ||||
|                 downloaded.enabled = !preferences.downloadedOnly().get() | ||||
|                 unread.checked = preferences.filterUnread().get() | ||||
|                 completed.checked = preferences.filterCompleted().get() | ||||
|                 if (preferences.downloadedOnly().get()) { | ||||
|                     downloaded.state = STATE_INCLUDE | ||||
|                     downloaded.enabled = false | ||||
|                 } else { | ||||
|                     downloaded.state = preferences.filterDownloaded().get() | ||||
|                 } | ||||
|                 unread.state = preferences.filterUnread().get() | ||||
|                 completed.state = preferences.filterCompleted().get() | ||||
|             } | ||||
|  | ||||
|             override fun onItemClicked(item: Item) { | ||||
|                 item as Item.CheckboxGroup | ||||
|                 item.checked = !item.checked | ||||
|                 item as Item.TriStateGroup | ||||
|                 val newState = when (item.state) { | ||||
|                     STATE_IGNORE -> STATE_INCLUDE | ||||
|                     STATE_INCLUDE -> STATE_EXCLUDE | ||||
|                     STATE_EXCLUDE -> STATE_IGNORE | ||||
|                     else -> throw Exception("Unknown State") | ||||
|                 } | ||||
|                 item.state = newState | ||||
|                 when (item) { | ||||
|                     downloaded -> preferences.filterDownloaded().set(item.checked) | ||||
|                     unread -> preferences.filterUnread().set(item.checked) | ||||
|                     completed -> preferences.filterCompleted().set(item.checked) | ||||
|                     downloaded -> preferences.filterDownloaded().set(newState) | ||||
|                     unread -> preferences.filterUnread().set(newState) | ||||
|                     completed -> preferences.filterCompleted().set(newState) | ||||
|                 } | ||||
|  | ||||
|                 adapter.notifyItemChanged(item) | ||||
|   | ||||
| @@ -44,13 +44,13 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import java.util.Date | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.android.synthetic.main.main_activity.appbar | ||||
| import kotlinx.android.synthetic.main.main_activity.tabs | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import timber.log.Timber | ||||
| import java.util.Date | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class MainActivity : BaseActivity<MainActivityBinding>() { | ||||
|  | ||||
| @@ -126,26 +126,28 @@ class MainActivity : BaseActivity<MainActivityBinding>() { | ||||
|             onBackPressed() | ||||
|         } | ||||
|  | ||||
|         router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { | ||||
|             override fun onChangeStarted( | ||||
|                 to: Controller?, | ||||
|                 from: Controller?, | ||||
|                 isPush: Boolean, | ||||
|                 container: ViewGroup, | ||||
|                 handler: ControllerChangeHandler | ||||
|             ) { | ||||
|                 syncActivityViewWithController(to, from) | ||||
|             } | ||||
|         router.addChangeListener( | ||||
|             object : ControllerChangeHandler.ControllerChangeListener { | ||||
|                 override fun onChangeStarted( | ||||
|                     to: Controller?, | ||||
|                     from: Controller?, | ||||
|                     isPush: Boolean, | ||||
|                     container: ViewGroup, | ||||
|                     handler: ControllerChangeHandler | ||||
|                 ) { | ||||
|                     syncActivityViewWithController(to, from) | ||||
|                 } | ||||
|  | ||||
|             override fun onChangeCompleted( | ||||
|                 to: Controller?, | ||||
|                 from: Controller?, | ||||
|                 isPush: Boolean, | ||||
|                 container: ViewGroup, | ||||
|                 handler: ControllerChangeHandler | ||||
|             ) { | ||||
|                 override fun onChangeCompleted( | ||||
|                     to: Controller?, | ||||
|                     from: Controller?, | ||||
|                     isPush: Boolean, | ||||
|                     container: ViewGroup, | ||||
|                     handler: ControllerChangeHandler | ||||
|                 ) { | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,6 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChapterDividerItemDecoration | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSettingsSheet | ||||
| @@ -65,13 +64,13 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.ui.recent.history.HistoryController | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.chapter.NoChaptersException | ||||
| import eu.kanade.tachiyomi.util.hasCustomCover | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.util.view.getCoordinates | ||||
| import eu.kanade.tachiyomi.util.view.shrinkOnScroll | ||||
| import eu.kanade.tachiyomi.util.view.snack | ||||
| import kotlin.math.min | ||||
| import kotlinx.android.synthetic.main.main_activity.root_coordinator | ||||
| import kotlinx.android.synthetic.main.main_activity.toolbar | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| @@ -83,6 +82,7 @@ import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import kotlin.math.min | ||||
|  | ||||
| class MangaController : | ||||
|     NucleusController<MangaControllerBinding, MangaPresenter>, | ||||
| @@ -207,7 +207,6 @@ class MangaController : | ||||
|  | ||||
|         binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context)) | ||||
|         binding.recycler.setHasFixedSize(true) | ||||
|         chaptersAdapter?.fastScroller = binding.fastScroller | ||||
|  | ||||
| @@ -238,7 +237,7 @@ class MangaController : | ||||
|  | ||||
|         binding.actionToolbar.offsetAppbarHeight(activity!!) | ||||
|  | ||||
|         settingsSheet = ChaptersSettingsSheet(activity!!, presenter) { group -> | ||||
|         settingsSheet = ChaptersSettingsSheet(router, presenter) { group -> | ||||
|             if (group is ChaptersSettingsSheet.Filter.FilterGroup) { | ||||
|                 updateFilterIconState() | ||||
|                 chaptersAdapter?.notifyDataSetChanged() | ||||
| @@ -292,10 +291,10 @@ class MangaController : | ||||
|                     // Get coordinates and start animation | ||||
|                     actionFab?.getCoordinates()?.let { coordinates -> | ||||
|                         if (!binding.revealView.showRevealEffect( | ||||
|                             coordinates.x, | ||||
|                             coordinates.y, | ||||
|                             revealAnimationListener | ||||
|                         ) | ||||
|                                 coordinates.x, | ||||
|                                 coordinates.y, | ||||
|                                 revealAnimationListener | ||||
|                             ) | ||||
|                         ) { | ||||
|                             openChapter(item.chapter) | ||||
|                         } | ||||
| @@ -343,7 +342,8 @@ class MangaController : | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareOptionsMenu(menu: Menu) { | ||||
|         // Hide download options for local manga | ||||
|         // Hide options for local manga | ||||
|         menu.findItem(R.id.action_share).isVisible = !isLocalSource | ||||
|         menu.findItem(R.id.download_group).isVisible = !isLocalSource | ||||
|  | ||||
|         // Hide options for non-library manga | ||||
| @@ -354,6 +354,7 @@ class MangaController : | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_share -> shareManga() | ||||
|             R.id.download_next, R.id.download_next_5, R.id.download_next_10, | ||||
|             R.id.download_custom, R.id.download_unread, R.id.download_all | ||||
|             -> downloadChapters(item.itemId) | ||||
| @@ -694,7 +695,11 @@ class MangaController : | ||||
|     fun onFetchChaptersError(error: Throwable) { | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|         activity?.toast(error.message) | ||||
|         if (error is NoChaptersException) { | ||||
|             activity?.toast(activity?.getString(R.string.no_chapters_error)) | ||||
|         } else { | ||||
|             activity?.toast(error.message) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun onChapterStatusChange(download: Download) { | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem | ||||
| import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import eu.kanade.tachiyomi.util.isLocal | ||||
| import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed | ||||
| @@ -26,7 +27,6 @@ import eu.kanade.tachiyomi.util.prepUpdateCover | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import eu.kanade.tachiyomi.util.shouldDownloadNewChapters | ||||
| import eu.kanade.tachiyomi.util.updateCoverLastModified | ||||
| import java.util.Date | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| @@ -34,6 +34,7 @@ import rx.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
|  | ||||
| class MangaPresenter( | ||||
|     val manga: Manga, | ||||
| @@ -82,6 +83,10 @@ class MangaPresenter( | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         if (!manga.favorite) { | ||||
|             ChapterSettingsHelper.applySettingDefaults(manga) | ||||
|         } | ||||
|  | ||||
|         // Manga info - start | ||||
|  | ||||
|         getMangaObservable() | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Canvas | ||||
| import android.graphics.Rect | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.view.View | ||||
| import androidx.core.view.forEach | ||||
| import androidx.core.view.marginBottom | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
|  | ||||
| /** | ||||
|  * Mimics a DividerItemDecoration that doesn't draw between the first two items. | ||||
|  * | ||||
|  * Used in MangaController since the manga info header and chapters header are the first two | ||||
|  * items in the list using a ConcatAdapter. | ||||
|  */ | ||||
| class ChapterDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { | ||||
|  | ||||
|     private val divider: Drawable | ||||
|  | ||||
|     init { | ||||
|         val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)) | ||||
|         divider = a.getDrawable(0)!! | ||||
|         a.recycle() | ||||
|     } | ||||
|  | ||||
|     override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { | ||||
|         if (parent.layoutManager == null) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         canvas.save() | ||||
|         parent.forEach { | ||||
|             val top = it.bottom + it.marginBottom | ||||
|             val bottom = top + divider.intrinsicHeight | ||||
|             val left = parent.paddingStart | ||||
|             val right = parent.width - parent.paddingEnd | ||||
|             divider.setBounds(left, top, right, bottom) | ||||
|             divider.draw(canvas) | ||||
|         } | ||||
|         canvas.restore() | ||||
|     } | ||||
|  | ||||
|     override fun getItemOffsets( | ||||
|         outRect: Rect, | ||||
|         view: View, | ||||
|         parent: RecyclerView, | ||||
|         state: RecyclerView.State | ||||
|     ) { | ||||
|         val position = parent.getChildAdapterPosition(view) | ||||
|  | ||||
|         if (position == 0) { | ||||
|             outRect.setEmpty() | ||||
|         } else { | ||||
|             outRect.set(0, 0, 0, divider.intrinsicHeight) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -9,11 +9,11 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder | ||||
| import java.util.Date | ||||
| import kotlinx.android.synthetic.main.chapters_item.bookmark_icon | ||||
| import kotlinx.android.synthetic.main.chapters_item.chapter_description | ||||
| import kotlinx.android.synthetic.main.chapters_item.chapter_title | ||||
| import kotlinx.android.synthetic.main.chapters_item.download_text | ||||
| import java.util.Date | ||||
|  | ||||
| class ChapterHolder( | ||||
|     view: View, | ||||
|   | ||||
| @@ -6,10 +6,10 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DateFormat | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class ChaptersAdapter( | ||||
|     controller: MangaController, | ||||
|   | ||||
| @@ -1,34 +1,39 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
| import androidx.core.view.isVisible | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaPresenter | ||||
| import eu.kanade.tachiyomi.util.view.popupMenu | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView | ||||
| import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog | ||||
|  | ||||
| class ChaptersSettingsSheet( | ||||
|     activity: Activity, | ||||
|     private val router: Router, | ||||
|     private val presenter: MangaPresenter, | ||||
|     onGroupClickListener: (ExtendedNavigationView.Group) -> Unit | ||||
| ) : TabbedBottomSheetDialog(activity) { | ||||
| ) : TabbedBottomSheetDialog(router) { | ||||
|  | ||||
|     val filters: Filter | ||||
|     private val sort: Sort | ||||
|     private val display: Display | ||||
|  | ||||
|     init { | ||||
|         filters = Filter(activity) | ||||
|         filters = Filter(router.activity!!) | ||||
|         filters.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         sort = Sort(activity) | ||||
|         sort = Sort(router.activity!!) | ||||
|         sort.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         display = Display(activity) | ||||
|         display = Display(router.activity!!) | ||||
|         display.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         binding.menu.isVisible = true | ||||
|         binding.menu.setOnClickListener { it.post { showPopupMenu(it) } } | ||||
|     } | ||||
|  | ||||
|     override fun getTabViews(): List<View> = listOf( | ||||
| @@ -43,6 +48,23 @@ class ChaptersSettingsSheet( | ||||
|         R.string.action_display | ||||
|     ) | ||||
|  | ||||
|     private fun showPopupMenu(view: View) { | ||||
|         view.popupMenu( | ||||
|             R.menu.default_chapter_filter, | ||||
|             { | ||||
|             }, | ||||
|             { | ||||
|                 when (this.itemId) { | ||||
|                     R.id.set_as_default -> { | ||||
|                         SetChapterSettingsDialog(presenter.manga).showDialog(router) | ||||
|                         true | ||||
|                     } | ||||
|                     else -> true | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Filters group (unread, downloaded, ...). | ||||
|      */ | ||||
|   | ||||
| @@ -66,7 +66,7 @@ class MangaChaptersHeaderAdapter( | ||||
|             } else { | ||||
|                 view.context.getResourceColor(R.attr.colorOnPrimary) | ||||
|             } | ||||
|             DrawableCompat.setTint(binding.btnChaptersFilter.icon, filterColor) | ||||
|             DrawableCompat.setTint(binding.btnChaptersFilter.drawable, filterColor) | ||||
|  | ||||
|             merge(view.clicks(), binding.btnChaptersFilter.clicks()) | ||||
|                 .onEach { controller.showSettingsSheet() } | ||||
|   | ||||
| @@ -0,0 +1,48 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.afollestad.materialdialogs.customview.customView | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.DialogCheckboxView | ||||
|  | ||||
| class SetChapterSettingsDialog(bundle: Bundle? = null) : DialogController(bundle) { | ||||
|  | ||||
|     constructor(manga: Manga) : this( | ||||
|         Bundle().apply { | ||||
|             putSerializable(MANGA_KEY, manga) | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         val view = DialogCheckboxView(activity!!).apply { | ||||
|             setDescription(R.string.confirm_set_chapter_settings) | ||||
|             setOptionDescription(R.string.also_set_chapter_settings_for_library) | ||||
|         } | ||||
|  | ||||
|         return MaterialDialog(activity!!) | ||||
|             .title(R.string.chapter_settings) | ||||
|             .customView( | ||||
|                 view = view, | ||||
|                 horizontalPadding = true | ||||
|             ) | ||||
|             .positiveButton(android.R.string.ok) { | ||||
|                 ChapterSettingsHelper.setGlobalSettings(args.getSerializable(MANGA_KEY)!! as Manga) | ||||
|                 if (view.isChecked()) { | ||||
|                     ChapterSettingsHelper.updateAllMangasWithGlobalDefaults() | ||||
|                 } | ||||
|  | ||||
|                 activity?.toast(activity!!.getString(R.string.chapter_settings_updated)) | ||||
|             } | ||||
|             .negativeButton(android.R.string.cancel) | ||||
|     } | ||||
|  | ||||
|     private companion object { | ||||
|         const val MANGA_KEY = "manga" | ||||
|     } | ||||
| } | ||||
| @@ -105,13 +105,27 @@ class MangaInfoHeaderAdapter( | ||||
|                     isVisible = true | ||||
|  | ||||
|                     if (trackCount > 0) { | ||||
|                         setIconResource(R.drawable.ic_done_24dp) | ||||
|                         text = view.context.resources.getQuantityString(R.plurals.num_trackers, trackCount, trackCount) | ||||
|                         isChecked = true | ||||
|                         setCompoundDrawablesWithIntrinsicBounds( | ||||
|                             null, | ||||
|                             ContextCompat.getDrawable(context, R.drawable.ic_done_24dp), | ||||
|                             null, | ||||
|                             null | ||||
|                         ) | ||||
|                         text = view.context.resources.getQuantityString( | ||||
|                             R.plurals.num_trackers, | ||||
|                             trackCount, | ||||
|                             trackCount | ||||
|                         ) | ||||
|                         isSelected = true | ||||
|                     } else { | ||||
|                         setIconResource(R.drawable.ic_sync_24dp) | ||||
|                         setCompoundDrawablesWithIntrinsicBounds( | ||||
|                             null, | ||||
|                             ContextCompat.getDrawable(context, R.drawable.ic_sync_24dp), | ||||
|                             null, | ||||
|                             null | ||||
|                         ) | ||||
|                         text = view.context.getString(R.string.manga_tracking_tab) | ||||
|                         isChecked = false | ||||
|                         isSelected = false | ||||
|                     } | ||||
|  | ||||
|                     clicks() | ||||
| @@ -128,12 +142,6 @@ class MangaInfoHeaderAdapter( | ||||
|                     .onEach { controller.openMangaInWebView() } | ||||
|                     .launchIn(scope) | ||||
|                 binding.btnWebview.setTooltip(R.string.action_open_in_web_view) | ||||
|  | ||||
|                 binding.btnShare.isVisible = true | ||||
|                 binding.btnShare.clicks() | ||||
|                     .onEach { controller.shareManga() } | ||||
|                     .launchIn(scope) | ||||
|                 binding.btnShare.setTooltip(R.string.action_share) | ||||
|             } | ||||
|  | ||||
|             binding.mangaFullTitle.longClicks() | ||||
| @@ -285,14 +293,24 @@ class MangaInfoHeaderAdapter( | ||||
|  | ||||
|                 // Update genres list | ||||
|                 if (!manga.genre.isNullOrBlank()) { | ||||
|                     binding.mangaGenresTagsCompactChips.setChips(manga.getGenres(), controller::performSearch) | ||||
|                     binding.mangaGenresTagsFullChips.setChips(manga.getGenres(), controller::performSearch) | ||||
|                     binding.mangaGenresTagsCompactChips.setChips( | ||||
|                         manga.getGenres(), | ||||
|                         controller::performSearch | ||||
|                     ) | ||||
|                     binding.mangaGenresTagsFullChips.setChips( | ||||
|                         manga.getGenres(), | ||||
|                         controller::performSearch | ||||
|                     ) | ||||
|                 } else { | ||||
|                     binding.mangaGenresTagsWrapper.isVisible = false | ||||
|                 } | ||||
|  | ||||
|                 // Handle showing more or less info | ||||
|                 merge(binding.mangaSummarySection.clicks(), binding.mangaSummaryText.clicks(), binding.mangaInfoToggle.clicks()) | ||||
|                 merge( | ||||
|                     binding.mangaSummarySection.clicks(), | ||||
|                     binding.mangaSummaryText.clicks(), | ||||
|                     binding.mangaInfoToggle.clicks() | ||||
|                 ) | ||||
|                     .onEach { toggleMangaInfo(view.context) } | ||||
|                     .launchIn(scope) | ||||
|  | ||||
| @@ -310,20 +328,22 @@ class MangaInfoHeaderAdapter( | ||||
|  | ||||
|         private fun toggleMangaInfo(context: Context) { | ||||
|             val isExpanded = | ||||
|                 binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse) | ||||
|                 binding.mangaInfoToggle.contentDescription == context.getString(R.string.manga_info_collapse) | ||||
|  | ||||
|             with(binding.mangaInfoToggle) { | ||||
|                 text = if (isExpanded) { | ||||
|                 contentDescription = if (isExpanded) { | ||||
|                     context.getString(R.string.manga_info_expand) | ||||
|                 } else { | ||||
|                     context.getString(R.string.manga_info_collapse) | ||||
|                 } | ||||
|  | ||||
|                 icon = if (isExpanded) { | ||||
|                     context.getDrawable(R.drawable.ic_baseline_expand_more_24dp) | ||||
|                 } else { | ||||
|                     context.getDrawable(R.drawable.ic_baseline_expand_less_24dp) | ||||
|                 } | ||||
|                 setImageDrawable( | ||||
|                     if (isExpanded) { | ||||
|                         context.getDrawable(R.drawable.ic_baseline_expand_more_24dp) | ||||
|                     } else { | ||||
|                         context.getDrawable(R.drawable.ic_baseline_expand_less_24dp) | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             with(binding.mangaSummaryText) { | ||||
| @@ -355,13 +375,18 @@ class MangaInfoHeaderAdapter( | ||||
|             // Set the Favorite drawable to the correct one. | ||||
|             // Border drawable if false, filled drawable if true. | ||||
|             binding.btnFavorite.apply { | ||||
|                 icon = ContextCompat.getDrawable( | ||||
|                     context, | ||||
|                     if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp | ||||
|                 setCompoundDrawablesWithIntrinsicBounds( | ||||
|                     null, | ||||
|                     ContextCompat.getDrawable( | ||||
|                         context, | ||||
|                         if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp | ||||
|                     ), | ||||
|                     null, | ||||
|                     null | ||||
|                 ) | ||||
|                 text = | ||||
|                     context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library) | ||||
|                 isChecked = isFavorite | ||||
|                 isSelected = isFavorite | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import java.util.Calendar | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Calendar | ||||
|  | ||||
| class SetTrackReadingDatesDialog<T> : DialogController | ||||
|         where T : Controller, T : SetTrackReadingDatesDialog.Listener { | ||||
|   | ||||
| @@ -5,8 +5,8 @@ import androidx.core.view.isVisible | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.databinding.TrackItemBinding | ||||
| import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder | ||||
| import java.text.DateFormat | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DateFormat | ||||
|  | ||||
| class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) : BaseViewHolder(binding.root) { | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.android.synthetic.main.track_search_dialog.view.progress | ||||
| import kotlinx.android.synthetic.main.track_search_dialog.view.track_search | ||||
| import kotlinx.android.synthetic.main.track_search_dialog.view.track_search_list | ||||
| @@ -26,6 +25,7 @@ import reactivecircus.flowbinding.android.widget.itemClicks | ||||
| import reactivecircus.flowbinding.android.widget.textChanges | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class TrackSearchDialog : DialogController { | ||||
|  | ||||
|   | ||||
| @@ -10,9 +10,9 @@ import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.mikepenz.aboutlibraries.LibsBuilder | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateChecker | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateResult | ||||
| import eu.kanade.tachiyomi.data.updater.UpdaterService | ||||
| import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsController | ||||
| import eu.kanade.tachiyomi.util.lang.launchNow | ||||
| @@ -23,19 +23,19 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import timber.log.Timber | ||||
| import java.text.DateFormat | ||||
| import java.text.ParseException | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import java.util.TimeZone | ||||
| import timber.log.Timber | ||||
|  | ||||
| class AboutController : SettingsController() { | ||||
|  | ||||
|     /** | ||||
|      * Checks for new releases | ||||
|      */ | ||||
|     private val updateChecker by lazy { UpdateChecker.getUpdateChecker() } | ||||
|     private val updateChecker by lazy { GithubUpdateChecker() } | ||||
|  | ||||
|     private val dateFormat: DateFormat = preferences.dateFormat() | ||||
|  | ||||
| @@ -234,7 +234,9 @@ class AboutController : SettingsController() { | ||||
|             val buildTime = inputDf.parse(BuildConfig.BUILD_TIME) | ||||
|  | ||||
|             val outputDf = DateFormat.getDateTimeInstance( | ||||
|                 DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault() | ||||
|                 DateFormat.MEDIUM, | ||||
|                 DateFormat.SHORT, | ||||
|                 Locale.getDefault() | ||||
|             ) | ||||
|             outputDf.timeZone = TimeZone.getDefault() | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction | ||||
| @@ -28,6 +27,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys | ||||
|  | ||||
| class MoreController : | ||||
|     SettingsController(), | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import android.view.animation.Animation | ||||
| import android.view.animation.AnimationUtils | ||||
| import android.widget.SeekBar | ||||
| import androidx.core.view.ViewCompat | ||||
| import androidx.core.view.WindowInsetsCompat | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.core.view.setPadding | ||||
| import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView | ||||
| @@ -56,8 +57,6 @@ import eu.kanade.tachiyomi.util.view.showBar | ||||
| import eu.kanade.tachiyomi.util.view.snack | ||||
| import eu.kanade.tachiyomi.widget.SimpleAnimationListener | ||||
| import eu.kanade.tachiyomi.widget.SimpleSeekBarListener | ||||
| import java.io.File | ||||
| import kotlin.math.abs | ||||
| import kotlinx.coroutines.FlowPreview | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.flow.drop | ||||
| @@ -67,6 +66,8 @@ import kotlinx.coroutines.flow.sample | ||||
| import nucleus.factory.RequiresPresenter | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import kotlin.math.abs | ||||
|  | ||||
| /** | ||||
|  * Activity containing the reader of Tachiyomi. This activity is mostly a container of the | ||||
| @@ -290,24 +291,27 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() | ||||
|  | ||||
|         ViewCompat.setOnApplyWindowInsetsListener(binding.readerMenu) { _, insets -> | ||||
|             if (!window.isDefaultBar()) { | ||||
|                 val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||||
|                 binding.readerMenu.setPadding( | ||||
|                     insets.systemWindowInsetLeft, | ||||
|                     insets.systemWindowInsetTop, | ||||
|                     insets.systemWindowInsetRight, | ||||
|                     insets.systemWindowInsetBottom | ||||
|                     systemInsets.left, | ||||
|                     systemInsets.top, | ||||
|                     systemInsets.right, | ||||
|                     systemInsets.bottom | ||||
|                 ) | ||||
|             } | ||||
|             insets | ||||
|         } | ||||
|  | ||||
|         // Init listeners on bottom menu | ||||
|         binding.pageSeekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (viewer != null && fromUser) { | ||||
|                     moveToPageIndex(value) | ||||
|         binding.pageSeekbar.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (viewer != null && fromUser) { | ||||
|                         moveToPageIndex(value) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|         binding.leftChapter.setOnClickListener { | ||||
|             if (viewer != null) { | ||||
|                 if (viewer is R2LPagerViewer) { | ||||
| @@ -347,12 +351,14 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() | ||||
|  | ||||
|             if (animate) { | ||||
|                 val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top) | ||||
|                 toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { | ||||
|                     override fun onAnimationStart(animation: Animation) { | ||||
|                         // Fix status bar being translucent the first time it's opened. | ||||
|                         window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) | ||||
|                 toolbarAnimation.setAnimationListener( | ||||
|                     object : SimpleAnimationListener() { | ||||
|                         override fun onAnimationStart(animation: Animation) { | ||||
| // Fix status bar being translucent the first time it's opened. | ||||
|                             window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 ) | ||||
|                 binding.toolbar.startAnimation(toolbarAnimation) | ||||
|  | ||||
|                 val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom) | ||||
| @@ -371,11 +377,13 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() | ||||
|  | ||||
|             if (animate) { | ||||
|                 val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top) | ||||
|                 toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { | ||||
|                     override fun onAnimationEnd(animation: Animation) { | ||||
|                         binding.readerMenu.isVisible = false | ||||
|                 toolbarAnimation.setAnimationListener( | ||||
|                     object : SimpleAnimationListener() { | ||||
|                         override fun onAnimationEnd(animation: Animation) { | ||||
|                             binding.readerMenu.isVisible = false | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 ) | ||||
|                 binding.toolbar.startAnimation(toolbarAnimation) | ||||
|  | ||||
|                 val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) | ||||
| @@ -572,9 +580,13 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() | ||||
|      * Called from the presenter when a page is ready to be shared. It shows Android's default | ||||
|      * sharing tool. | ||||
|      */ | ||||
|     fun onShareImageResult(file: File) { | ||||
|     fun onShareImageResult(file: File, page: ReaderPage) { | ||||
|         val manga = presenter.manga ?: return | ||||
|         val chapter = page.chapter.chapter | ||||
|  | ||||
|         val stream = file.getUriCompat(this) | ||||
|         val intent = Intent(Intent.ACTION_SEND).apply { | ||||
|             putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number)) | ||||
|             putExtra(Intent.EXTRA_STREAM, stream) | ||||
|             flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||
|             type = "image/*" | ||||
|   | ||||
| @@ -74,45 +74,55 @@ class ReaderColorFilterSheet(private val activity: ReaderActivity) : BottomSheet | ||||
|         } | ||||
|         binding.colorFilterMode.setSelection(preferences.colorFilterMode().get(), false) | ||||
|  | ||||
|         binding.seekbarColorFilterAlpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (fromUser) { | ||||
|                     setColorValue(value, ALPHA_MASK, 24) | ||||
|         binding.seekbarColorFilterAlpha.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (fromUser) { | ||||
|                         setColorValue(value, ALPHA_MASK, 24) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         binding.seekbarColorFilterRed.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (fromUser) { | ||||
|                     setColorValue(value, RED_MASK, 16) | ||||
|         binding.seekbarColorFilterRed.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (fromUser) { | ||||
|                         setColorValue(value, RED_MASK, 16) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         binding.seekbarColorFilterGreen.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (fromUser) { | ||||
|                     setColorValue(value, GREEN_MASK, 8) | ||||
|         binding.seekbarColorFilterGreen.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (fromUser) { | ||||
|                         setColorValue(value, GREEN_MASK, 8) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         binding.seekbarColorFilterBlue.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (fromUser) { | ||||
|                     setColorValue(value, BLUE_MASK, 0) | ||||
|         binding.seekbarColorFilterBlue.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (fromUser) { | ||||
|                         setColorValue(value, BLUE_MASK, 0) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         binding.brightnessSeekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { | ||||
|             override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                 if (fromUser) { | ||||
|                     preferences.customBrightnessValue().set(value) | ||||
|         binding.brightnessSeekbar.setOnSeekBarChangeListener( | ||||
|             object : SimpleSeekBarListener() { | ||||
|                 override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { | ||||
|                     if (fromUser) { | ||||
|                         preferences.customBrightnessValue().set(value) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onStart() { | ||||
|   | ||||
| @@ -27,9 +27,6 @@ import eu.kanade.tachiyomi.util.lang.takeBytes | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import eu.kanade.tachiyomi.util.updateCoverLastModified | ||||
| import java.io.File | ||||
| import java.util.Date | ||||
| import java.util.concurrent.TimeUnit | ||||
| import rx.Completable | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| @@ -38,6 +35,9 @@ import rx.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
| import java.util.Date | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * Presenter used by the activity to perform background operations. | ||||
| @@ -569,7 +569,7 @@ class ReaderPresenter( | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribeFirst( | ||||
|                 { view, file -> view.onShareImageResult(file) }, | ||||
|                 { view, file -> view.onShareImageResult(file, page) }, | ||||
|                 { _, _ -> /* Empty */ } | ||||
|             ) | ||||
|     } | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import rx.Observable | ||||
| import java.io.File | ||||
| import java.io.FileInputStream | ||||
| import rx.Observable | ||||
|  | ||||
| /** | ||||
|  * Loader used to load a chapter from a directory given on [file]. | ||||
|   | ||||
| @@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.reader.loader | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.storage.EpubFile | ||||
| import java.io.File | ||||
| import rx.Observable | ||||
| import java.io.File | ||||
|  | ||||
| /** | ||||
|  * Loader used to load a chapter from a .epub file. | ||||
|   | ||||
| @@ -6,9 +6,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.plusAssign | ||||
| import java.util.concurrent.PriorityBlockingQueue | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
| import kotlin.math.min | ||||
| import rx.Completable | ||||
| import rx.Observable | ||||
| import rx.schedulers.Schedulers | ||||
| @@ -18,6 +15,9 @@ import rx.subscriptions.CompositeSubscription | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.PriorityBlockingQueue | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
| import kotlin.math.min | ||||
|  | ||||
| /** | ||||
|  * Loader used to load chapters from an online source. | ||||
|   | ||||
| @@ -4,14 +4,14 @@ import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import junrar.Archive | ||||
| import junrar.rarfile.FileHeader | ||||
| import rx.Observable | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import java.io.PipedInputStream | ||||
| import java.io.PipedOutputStream | ||||
| import java.util.concurrent.Executors | ||||
| import junrar.Archive | ||||
| import junrar.rarfile.FileHeader | ||||
| import rx.Observable | ||||
|  | ||||
| /** | ||||
|  * Loader used to load a chapter from a .rar or .cbr file. | ||||
|   | ||||
| @@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import rx.Observable | ||||
| import java.io.File | ||||
| import java.nio.charset.StandardCharsets | ||||
| import java.util.zip.ZipEntry | ||||
| import java.util.zip.ZipFile | ||||
| import rx.Observable | ||||
|  | ||||
| /** | ||||
|  * Loader used to load a chapter from a .zip or .cbz file. | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.viewer | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import kotlin.math.floor | ||||
|  | ||||
| object MissingChapters { | ||||
|  | ||||
|     fun hasMissingChapters(higher: Chapter, lower: Chapter): Boolean { | ||||
|         return hasMissingChapters(higher.chapter_number, lower.chapter_number) | ||||
|     } | ||||
|  | ||||
|     fun hasMissingChapters(higherChapterNumber: Float, lowerChapterNumber: Float): Boolean { | ||||
|         return floor(higherChapterNumber) - floor(lowerChapterNumber) - 1f > 0f | ||||
|     } | ||||
| } | ||||
| @@ -63,9 +63,12 @@ class ReaderProgressBar @JvmOverloads constructor( | ||||
|      */ | ||||
|     private val rotationAnimation by lazy { | ||||
|         RotateAnimation( | ||||
|             0f, 360f, | ||||
|             Animation.RELATIVE_TO_SELF, 0.5f, | ||||
|             Animation.RELATIVE_TO_SELF, 0.5f | ||||
|             0f, | ||||
|             360f, | ||||
|             Animation.RELATIVE_TO_SELF, | ||||
|             0.5f, | ||||
|             Animation.RELATIVE_TO_SELF, | ||||
|             0.5f | ||||
|         ).apply { | ||||
|             interpolator = LinearInterpolator() | ||||
|             repeatCount = Animation.INFINITE | ||||
|   | ||||
| @@ -0,0 +1,103 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.viewer | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.widget.LinearLayout | ||||
| import androidx.core.text.bold | ||||
| import androidx.core.text.buildSpannedString | ||||
| import androidx.core.view.isVisible | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition | ||||
| import kotlinx.android.synthetic.main.reader_transition_view.view.lower_text | ||||
| import kotlinx.android.synthetic.main.reader_transition_view.view.upper_text | ||||
| import kotlinx.android.synthetic.main.reader_transition_view.view.warning | ||||
| import kotlinx.android.synthetic.main.reader_transition_view.view.warning_text | ||||
| import kotlin.math.floor | ||||
|  | ||||
| class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     LinearLayout(context, attrs) { | ||||
|  | ||||
|     init { | ||||
|         inflate(context, R.layout.reader_transition_view, this) | ||||
|     } | ||||
|  | ||||
|     fun bind(transition: ChapterTransition) { | ||||
|         when (transition) { | ||||
|             is ChapterTransition.Prev -> bindPrevChapterTransition(transition) | ||||
|             is ChapterTransition.Next -> bindNextChapterTransition(transition) | ||||
|         } | ||||
|  | ||||
|         missingChapterWarning(transition) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds a previous chapter transition on this view and subscribes to the page load status. | ||||
|      */ | ||||
|     private fun bindPrevChapterTransition(transition: ChapterTransition) { | ||||
|         val prevChapter = transition.to | ||||
|  | ||||
|         val hasPrevChapter = prevChapter != null | ||||
|         lower_text.isVisible = hasPrevChapter | ||||
|         if (hasPrevChapter) { | ||||
|             upper_text.textAlignment = TEXT_ALIGNMENT_TEXT_START | ||||
|             upper_text.text = buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_current)) } | ||||
|                 append("\n${transition.from.chapter.name}") | ||||
|             } | ||||
|             lower_text.text = buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_previous)) } | ||||
|                 append("\n${prevChapter!!.chapter.name}") | ||||
|             } | ||||
|         } else { | ||||
|             upper_text.textAlignment = TEXT_ALIGNMENT_CENTER | ||||
|             upper_text.text = context.getString(R.string.transition_no_previous) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds a next chapter transition on this view and subscribes to the load status. | ||||
|      */ | ||||
|     private fun bindNextChapterTransition(transition: ChapterTransition) { | ||||
|         val nextChapter = transition.to | ||||
|  | ||||
|         val hasNextChapter = nextChapter != null | ||||
|         lower_text.isVisible = hasNextChapter | ||||
|         if (hasNextChapter) { | ||||
|             upper_text.textAlignment = TEXT_ALIGNMENT_TEXT_START | ||||
|             upper_text.text = buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_finished)) } | ||||
|                 append("\n${transition.from.chapter.name}") | ||||
|             } | ||||
|             lower_text.text = buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_next)) } | ||||
|                 append("\n${nextChapter!!.chapter.name}") | ||||
|             } | ||||
|         } else { | ||||
|             upper_text.textAlignment = TEXT_ALIGNMENT_CENTER | ||||
|             upper_text.text = context.getString(R.string.transition_no_next) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun missingChapterWarning(transition: ChapterTransition) { | ||||
|         if (transition.to == null) { | ||||
|             warning.isVisible = false | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val fromChapterNumber: Float = floor(transition.from.chapter.chapter_number) | ||||
|         val toChapterNumber: Float = floor(transition.to!!.chapter.chapter_number) | ||||
|  | ||||
|         val chapterDifference = when (transition) { | ||||
|             is ChapterTransition.Prev -> fromChapterNumber - toChapterNumber - 1f | ||||
|             is ChapterTransition.Next -> toChapterNumber - fromChapterNumber - 1f | ||||
|         } | ||||
|  | ||||
|         val hasMissingChapters = when (transition) { | ||||
|             is ChapterTransition.Prev -> MissingChapters.hasMissingChapters(fromChapterNumber, toChapterNumber) | ||||
|             is ChapterTransition.Next -> MissingChapters.hasMissingChapters(toChapterNumber, fromChapterNumber) | ||||
|         } | ||||
|  | ||||
|         warning_text.text = resources.getQuantityString(R.plurals.missing_chapters_warning, chapterDifference.toInt(), chapterDifference.toInt()) | ||||
|         warning.isVisible = hasMissingChapters | ||||
|     } | ||||
| } | ||||
| @@ -35,12 +35,12 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import eu.kanade.tachiyomi.widget.ViewPagerAdapter | ||||
| import java.io.InputStream | ||||
| import java.util.concurrent.TimeUnit | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import java.io.InputStream | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * View of the ViewPager that contains a page of a chapter. | ||||
| @@ -307,20 +307,22 @@ class PagerPageHolder( | ||||
|             setMinimumDpi(90) | ||||
|             setMinimumTileDpi(180) | ||||
|             setCropBorders(config.imageCropBorders) | ||||
|             setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { | ||||
|                 override fun onReady() { | ||||
|                     when (config.imageZoomType) { | ||||
|                         ZoomType.Left -> setScaleAndCenter(scale, PointF(0f, 0f)) | ||||
|                         ZoomType.Right -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) | ||||
|                         ZoomType.Center -> setScaleAndCenter(scale, center.also { it?.y = 0f }) | ||||
|             setOnImageEventListener( | ||||
|                 object : SubsamplingScaleImageView.DefaultOnImageEventListener() { | ||||
|                     override fun onReady() { | ||||
|                         when (config.imageZoomType) { | ||||
|                             ZoomType.Left -> setScaleAndCenter(scale, PointF(0f, 0f)) | ||||
|                             ZoomType.Right -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) | ||||
|                             ZoomType.Center -> setScaleAndCenter(scale, center.also { it?.y = 0f }) | ||||
|                         } | ||||
|                         onImageDecoded() | ||||
|                     } | ||||
|                     onImageDecoded() | ||||
|                 } | ||||
|  | ||||
|                 override fun onImageLoadError(e: Exception) { | ||||
|                     onImageDecodeError() | ||||
|                     override fun onImageLoadError(e: Exception) { | ||||
|                         onImageDecodeError() | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|             ) | ||||
|         } | ||||
|         addView(subsamplingImageView) | ||||
|         return subsamplingImageView!! | ||||
| @@ -338,16 +340,18 @@ class PagerPageHolder( | ||||
|             setZoomTransitionDuration(viewer.config.doubleTapAnimDuration) | ||||
|             setScaleLevels(1f, 2f, 3f) | ||||
|             // Force 2 scale levels on double tap | ||||
|             setOnDoubleTapListener(object : GestureDetector.SimpleOnGestureListener() { | ||||
|                 override fun onDoubleTap(e: MotionEvent): Boolean { | ||||
|                     if (scale > 1f) { | ||||
|                         setScale(1f, e.x, e.y, true) | ||||
|                     } else { | ||||
|                         setScale(2f, e.x, e.y, true) | ||||
|             setOnDoubleTapListener( | ||||
|                 object : GestureDetector.SimpleOnGestureListener() { | ||||
|                     override fun onDoubleTap(e: MotionEvent): Boolean { | ||||
|                         if (scale > 1f) { | ||||
|                             setScale(1f, e.x, e.y, true) | ||||
|                         } else { | ||||
|                             setScale(2f, e.x, e.y, true) | ||||
|                         } | ||||
|                         return true | ||||
|                     } | ||||
|                     return true | ||||
|                 } | ||||
|             }) | ||||
|             ) | ||||
|         } | ||||
|         addView(imageView) | ||||
|         return imageView!! | ||||
| @@ -437,31 +441,33 @@ class PagerPageHolder( | ||||
|             .skipMemoryCache(true) | ||||
|             .diskCacheStrategy(DiskCacheStrategy.NONE) | ||||
|             .transition(DrawableTransitionOptions.with(NoTransition.getFactory())) | ||||
|             .listener(object : RequestListener<Drawable> { | ||||
|                 override fun onLoadFailed( | ||||
|                     e: GlideException?, | ||||
|                     model: Any?, | ||||
|                     target: Target<Drawable>?, | ||||
|                     isFirstResource: Boolean | ||||
|                 ): Boolean { | ||||
|                     onImageDecodeError() | ||||
|                     return false | ||||
|                 } | ||||
|  | ||||
|                 override fun onResourceReady( | ||||
|                     resource: Drawable?, | ||||
|                     model: Any?, | ||||
|                     target: Target<Drawable>?, | ||||
|                     dataSource: DataSource?, | ||||
|                     isFirstResource: Boolean | ||||
|                 ): Boolean { | ||||
|                     if (resource is GifDrawable) { | ||||
|                         resource.setLoopCount(GifDrawable.LOOP_INTRINSIC) | ||||
|             .listener( | ||||
|                 object : RequestListener<Drawable> { | ||||
|                     override fun onLoadFailed( | ||||
|                         e: GlideException?, | ||||
|                         model: Any?, | ||||
|                         target: Target<Drawable>?, | ||||
|                         isFirstResource: Boolean | ||||
|                     ): Boolean { | ||||
|                         onImageDecodeError() | ||||
|                         return false | ||||
|                     } | ||||
|  | ||||
|                     override fun onResourceReady( | ||||
|                         resource: Drawable?, | ||||
|                         model: Any?, | ||||
|                         target: Target<Drawable>?, | ||||
|                         dataSource: DataSource?, | ||||
|                         isFirstResource: Boolean | ||||
|                     ): Boolean { | ||||
|                         if (resource is GifDrawable) { | ||||
|                             resource.setLoopCount(GifDrawable.LOOP_INTRINSIC) | ||||
|                         } | ||||
|                         onImageDecoded() | ||||
|                         return false | ||||
|                     } | ||||
|                     onImageDecoded() | ||||
|                     return false | ||||
|                 } | ||||
|             }) | ||||
|             ) | ||||
|             .into(this) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,13 +8,11 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| import android.view.ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| import android.widget.LinearLayout | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.TextView | ||||
| import androidx.appcompat.widget.AppCompatTextView | ||||
| import androidx.core.text.bold | ||||
| import androidx.core.text.buildSpannedString | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import eu.kanade.tachiyomi.widget.ViewPagerAdapter | ||||
| import rx.Subscription | ||||
| @@ -40,14 +38,6 @@ class PagerTransitionHolder( | ||||
|      */ | ||||
|     private var statusSubscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Text view used to display the text of the current and next/prev chapters. | ||||
|      */ | ||||
|     private var textView = TextView(context).apply { | ||||
|         textSize = 17.5F | ||||
|         wrapContent() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * View container of the current status of the transition page. Child views will be added | ||||
|      * dynamically. | ||||
| @@ -63,13 +53,14 @@ class PagerTransitionHolder( | ||||
|         gravity = Gravity.CENTER | ||||
|         val sidePadding = 64.dpToPx | ||||
|         setPadding(sidePadding, 0, sidePadding, 0) | ||||
|         addView(textView) | ||||
|  | ||||
|         val transitionView = ReaderTransitionView(context) | ||||
|         addView(transitionView) | ||||
|         addView(pagesContainer) | ||||
|  | ||||
|         when (transition) { | ||||
|             is ChapterTransition.Prev -> bindPrevChapterTransition() | ||||
|             is ChapterTransition.Next -> bindNextChapterTransition() | ||||
|         } | ||||
|         transitionView.bind(transition) | ||||
|  | ||||
|         transition.to?.let { observeStatus(it) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -81,50 +72,6 @@ class PagerTransitionHolder( | ||||
|         statusSubscription = null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds a next chapter transition on this view and subscribes to the load status. | ||||
|      */ | ||||
|     private fun bindNextChapterTransition() { | ||||
|         val nextChapter = transition.to | ||||
|  | ||||
|         textView.text = if (nextChapter != null) { | ||||
|             buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_finished)) } | ||||
|                 append("\n${transition.from.chapter.name}\n\n") | ||||
|                 bold { append(context.getString(R.string.transition_next)) } | ||||
|                 append("\n${nextChapter.chapter.name}\n\n") | ||||
|             } | ||||
|         } else { | ||||
|             context.getString(R.string.transition_no_next) | ||||
|         } | ||||
|  | ||||
|         if (nextChapter != null) { | ||||
|             observeStatus(nextChapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds a previous chapter transition on this view and subscribes to the page load status. | ||||
|      */ | ||||
|     private fun bindPrevChapterTransition() { | ||||
|         val prevChapter = transition.to | ||||
|  | ||||
|         textView.text = if (prevChapter != null) { | ||||
|             buildSpannedString { | ||||
|                 bold { append(context.getString(R.string.transition_current)) } | ||||
|                 append("\n${transition.from.chapter.name}\n\n") | ||||
|                 bold { append(context.getString(R.string.transition_previous)) } | ||||
|                 append("\n${prevChapter.chapter.name}\n\n") | ||||
|             } | ||||
|         } else { | ||||
|             context.getString(R.string.transition_no_previous) | ||||
|         } | ||||
|  | ||||
|         if (prevChapter != null) { | ||||
|             observeStatus(prevChapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Observes the status of the page list of the next/previous chapter. Whenever there's a new | ||||
|      * state, the pages container is cleaned up before setting the new state. | ||||
|   | ||||
| @@ -71,15 +71,17 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { | ||||
|         pager.offscreenPageLimit = 1 | ||||
|         pager.id = R.id.reader_pager | ||||
|         pager.adapter = adapter | ||||
|         pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { | ||||
|             override fun onPageSelected(position: Int) { | ||||
|                 onPageChange(position) | ||||
|             } | ||||
|         pager.addOnPageChangeListener( | ||||
|             object : ViewPager.SimpleOnPageChangeListener() { | ||||
|                 override fun onPageSelected(position: Int) { | ||||
|                     onPageChange(position) | ||||
|                 } | ||||
|  | ||||
|             override fun onPageScrollStateChanged(state: Int) { | ||||
|                 isIdle = state == ViewPager.SCROLL_STATE_IDLE | ||||
|                 override fun onPageScrollStateChanged(state: Int) { | ||||
|                     isIdle = state == ViewPager.SCROLL_STATE_IDLE | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         ) | ||||
|         pager.tapListener = f@{ event -> | ||||
|             if (!config.tappingEnabled) { | ||||
|                 activity.toggleMenu() | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.MissingChapters | ||||
| import eu.kanade.tachiyomi.widget.ViewPagerAdapter | ||||
| import timber.log.Timber | ||||
|  | ||||
| @@ -33,6 +34,10 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { | ||||
|     fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) { | ||||
|         val newItems = mutableListOf<Any>() | ||||
|  | ||||
|         // Forces chapter transition if there is missing chapters | ||||
|         val prevHasMissingChapters = if (chapters.prevChapter != null) MissingChapters.hasMissingChapters(chapters.currChapter.chapter, chapters.prevChapter.chapter) else false | ||||
|         val nextHasMissingChapters = if (chapters.nextChapter != null) MissingChapters.hasMissingChapters(chapters.nextChapter.chapter, chapters.currChapter.chapter) else false | ||||
|  | ||||
|         // Add previous chapter pages and transition. | ||||
|         if (chapters.prevChapter != null) { | ||||
|             // We only need to add the last few pages of the previous chapter, because it'll be | ||||
| @@ -44,7 +49,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { | ||||
|         } | ||||
|  | ||||
|         // Skip transition page if the chapter is loaded & current page is not a transition page | ||||
|         if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { | ||||
|         if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { | ||||
|             newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter)) | ||||
|         } | ||||
|  | ||||
| @@ -59,7 +64,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { | ||||
|         // Add next chapter transition and pages. | ||||
|         nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter) | ||||
|             .also { | ||||
|                 if (forceTransition || | ||||
|                 if (nextHasMissingChapters || forceTransition || | ||||
|                     chapters.nextChapter?.state !is ReaderChapter.State.Loaded | ||||
|                 ) { | ||||
|                     newItems.add(it) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user