mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Merge upstream changes
This commit is contained in:
		| @@ -8,6 +8,7 @@ import com.evernote.android.job.JobManager | ||||
| import com.github.ajalt.reprint.core.Reprint | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob | ||||
| import eu.kanade.tachiyomi.util.LocaleHelper | ||||
| import io.realm.Realm | ||||
| @@ -29,6 +30,7 @@ open class App : Application() { | ||||
|         if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) | ||||
|  | ||||
|         setupJobManager() | ||||
|         setupNotificationChannels() | ||||
|         setupRealm() //Setup metadata DB (EH) | ||||
|         Reprint.initialize(this) //Setup fingerprint (EH) | ||||
|  | ||||
| @@ -58,6 +60,11 @@ open class App : Application() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected open fun setupNotificationChannels() { | ||||
|         Notifications.createChannels(this) | ||||
|     } | ||||
|  | ||||
|     // EXH | ||||
|     private fun setupRealm() { | ||||
|         Realm.init(this) | ||||
|         val config = RealmConfiguration.Builder() | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.tachiyomi | ||||
|  | ||||
| object Constants { | ||||
|     const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1 | ||||
|     const val NOTIFICATION_LIBRARY_RESULT_ID = 2 | ||||
|     const val NOTIFICATION_UPDATER_ID = 3 | ||||
|     const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4 | ||||
|     const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5 | ||||
|     const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6 | ||||
| } | ||||
| @@ -45,6 +45,16 @@ object Migrations { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 26) { | ||||
|                 // Delete external chapter cache dir. | ||||
|                 val extCache = context.externalCacheDir | ||||
|                 if (extCache != null) { | ||||
|                     val chapterCache = File(extCache, "chapter_disk_cache") | ||||
|                     if (chapterCache.exists()) { | ||||
|                         chapterCache.deleteRecursively() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|   | ||||
| @@ -84,9 +84,6 @@ class BackupCreateService : IntentService(NAME) { | ||||
|         // Create root object | ||||
|         val root = JsonObject() | ||||
|  | ||||
|         // Create information object | ||||
|         val information = JsonObject() | ||||
|  | ||||
|         // Create manga array | ||||
|         val mangaEntries = JsonArray() | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,6 @@ class BackupCreatorJob : Job() { | ||||
|             if (interval > 0) { | ||||
|                 JobRequest.Builder(TAG) | ||||
|                         .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) | ||||
|                         .setPersisted(true) | ||||
|                         .setUpdateCurrent(true) | ||||
|                         .build() | ||||
|                         .schedule() | ||||
|   | ||||
| @@ -182,29 +182,33 @@ class BackupRestoreService : Service() { | ||||
|     private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> { | ||||
|         val startTime = System.currentTimeMillis() | ||||
|  | ||||
|         val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) | ||||
|         val json = JsonParser().parse(reader).asJsonObject | ||||
|         return Observable.just(Unit) | ||||
|                 .map { | ||||
|                     val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) | ||||
|                     val json = JsonParser().parse(reader).asJsonObject | ||||
|  | ||||
|         // Get parser version | ||||
|         val version = json.get(VERSION)?.asInt ?: 1 | ||||
|                     // Get parser version | ||||
|                     val version = json.get(VERSION)?.asInt ?: 1 | ||||
|  | ||||
|         // Initialize manager | ||||
|         backupManager = BackupManager(this, version) | ||||
|                     // Initialize manager | ||||
|                     backupManager = BackupManager(this, version) | ||||
|  | ||||
|         val mangasJson = json.get(MANGAS).asJsonArray | ||||
|                     val mangasJson = json.get(MANGAS).asJsonArray | ||||
|  | ||||
|         restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|         restoreProgress = 0 | ||||
|         errors.clear() | ||||
|                     restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                     restoreProgress = 0 | ||||
|                     errors.clear() | ||||
|  | ||||
|         // Restore categories | ||||
|         json.get(CATEGORIES)?.let { | ||||
|             backupManager.restoreCategories(it.asJsonArray) | ||||
|             restoreProgress += 1 | ||||
|             showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) | ||||
|         } | ||||
|                     // Restore categories | ||||
|                     json.get(CATEGORIES)?.let { | ||||
|                         backupManager.restoreCategories(it.asJsonArray) | ||||
|                         restoreProgress += 1 | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) | ||||
|                     } | ||||
|  | ||||
|         return Observable.from(mangasJson) | ||||
|                     mangasJson | ||||
|                 } | ||||
|                 .flatMap { Observable.from(it) } | ||||
|                 .concatMap { | ||||
|                     val obj = it.asJsonObject | ||||
|                     val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
| @@ -317,8 +321,8 @@ class BackupRestoreService : Service() { | ||||
|                     manga | ||||
|                 } | ||||
|                 .filter { it.id != null } | ||||
|                 .flatMap { manga -> | ||||
|                     chapterFetchObservable(source, manga, chapters) | ||||
|                 .flatMap { | ||||
|                     chapterFetchObservable(source, it, chapters) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|   | ||||
| @@ -44,13 +44,8 @@ class ChapterCache(private val context: Context) { | ||||
|     /** Google Json class used for parsing JSON files.  */ | ||||
|     private val gson: Gson by injectLazy() | ||||
|  | ||||
|     /** Parent directory of the cache. Ensure not null and not root directory or fallback | ||||
|      * to internal cache directory. **/ | ||||
|     private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 } | ||||
|             ?: context.cacheDir | ||||
|  | ||||
|     /** Cache class used for cache management.  */ | ||||
|     private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY), | ||||
|     private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), | ||||
|             PARAMETER_APP_VERSION, | ||||
|             PARAMETER_VALUE_COUNT, | ||||
|             PARAMETER_CACHE_SIZE) | ||||
| @@ -86,10 +81,10 @@ class ChapterCache(private val context: Context) { | ||||
|  | ||||
|         try { | ||||
|             // Remove the extension from the file to get the key of the cache | ||||
|             val key = file.substring(0, file.lastIndexOf(".")) | ||||
|             val key = file.substringBeforeLast(".") | ||||
|             // Remove file from cache. | ||||
|             return diskCache.remove(key) | ||||
|         } catch (e: IOException) { | ||||
|         } catch (e: Exception) { | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -20,7 +20,8 @@ class CoverCache(private val context: Context) { | ||||
|     /** | ||||
|      * Cache directory used for cache management. | ||||
|      */ | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") ?: | ||||
|             File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|  | ||||
|     /** | ||||
|      * Returns the cover from cache. | ||||
|   | ||||
| @@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| open class MangaGetResolver : DefaultGetResolver<Manga>() { | ||||
|  | ||||
|     override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply { | ||||
| interface BaseMangaGetResolver { | ||||
|     fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply { | ||||
|         id = cursor.getLong(cursor.getColumnIndex(COL_ID)) | ||||
|         source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)) | ||||
|         url = cursor.getString(cursor.getColumnIndex(COL_URL)) | ||||
| @@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver { | ||||
|  | ||||
|     override fun mapFromCursor(cursor: Cursor): Manga { | ||||
|         return mapBaseFromCursor(MangaImpl(), cursor) | ||||
|     } | ||||
| } | ||||
|  | ||||
| class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() | ||||
|   | ||||
| @@ -31,10 +31,7 @@ class ChapterImpl : Chapter { | ||||
|         if (other == null || javaClass != other.javaClass) return false | ||||
|  | ||||
|         val chapter = other as Chapter | ||||
|         // Forces updates on manga if scanlator changes. This will allow existing manga in library | ||||
|         // with scanlator to update. | ||||
|         return url == chapter.url && scanlator == chapter.scanlator | ||||
|  | ||||
|         return url == chapter.url | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| package eu.kanade.tachiyomi.data.database.models | ||||
|  | ||||
| class LibraryManga : MangaImpl() { | ||||
|  | ||||
|     var unread: Int = 0 | ||||
|  | ||||
|     var category: Int = 0 | ||||
|  | ||||
| } | ||||
| @@ -16,10 +16,6 @@ interface Manga : SManga { | ||||
|  | ||||
|     var chapter_flags: Int | ||||
|  | ||||
|     var unread: Int | ||||
|  | ||||
|     var category: Int | ||||
|  | ||||
|     fun setChapterOrder(order: Int) { | ||||
|         setFlags(order, SORT_MASK) | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.data.database.models | ||||
|  | ||||
| class MangaImpl : Manga { | ||||
| open class MangaImpl : Manga { | ||||
|  | ||||
|     override var id: Long? = null | ||||
|  | ||||
| @@ -32,10 +32,6 @@ class MangaImpl : Manga { | ||||
|  | ||||
|     override var chapter_flags: Int = 0 | ||||
|  | ||||
|     @Transient override var unread: Int = 0 | ||||
|  | ||||
|     @Transient override var category: Int = 0 | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other == null || javaClass != other.javaClass) return false | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery | ||||
| import com.pushtorefresh.storio.sqlite.queries.Query | ||||
| import com.pushtorefresh.storio.sqlite.queries.RawQuery | ||||
| import eu.kanade.tachiyomi.data.database.DbProvider | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver | ||||
| @@ -23,7 +24,7 @@ interface MangaQueries : DbProvider { | ||||
|             .prepare() | ||||
|  | ||||
|     fun getLibraryMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .listOfObjects(LibraryManga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(libraryQuery) | ||||
|                     .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|   | ||||
| @@ -1,24 +1,23 @@ | ||||
| package eu.kanade.tachiyomi.data.database.resolvers | ||||
|  | ||||
| import android.database.Cursor | ||||
| import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable | ||||
|  | ||||
| class LibraryMangaGetResolver : MangaGetResolver() { | ||||
| class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver { | ||||
|  | ||||
|     companion object { | ||||
|         val INSTANCE = LibraryMangaGetResolver() | ||||
|     } | ||||
|  | ||||
|     override fun mapFromCursor(cursor: Cursor): Manga { | ||||
|         val manga = super.mapFromCursor(cursor) | ||||
|     override fun mapFromCursor(cursor: Cursor): LibraryManga { | ||||
|         val manga = LibraryManga() | ||||
|  | ||||
|         val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD) | ||||
|         manga.unread = cursor.getInt(unreadColumn) | ||||
|  | ||||
|         val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY) | ||||
|         manga.category = cursor.getInt(categoryColumn) | ||||
|         mapBaseFromCursor(manga, cursor) | ||||
|         manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD)) | ||||
|         manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY)) | ||||
|  | ||||
|         return manga | ||||
|     } | ||||
|   | ||||
| @@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download | ||||
| import android.content.Context | ||||
| import android.graphics.BitmapFactory | ||||
| import android.support.v4.app.NotificationCompat | ||||
| import eu.kanade.tachiyomi.Constants | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.download.model.DownloadQueue | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationHandler | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.chop | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
| import java.util.regex.Pattern | ||||
| @@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      * Notification builder. | ||||
|      */ | ||||
|     private val notification by lazy { | ||||
|         NotificationCompat.Builder(context) | ||||
|         NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER) | ||||
|                 .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) | ||||
|     } | ||||
|  | ||||
| @@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      * | ||||
|      * @param id the id of the notification. | ||||
|      */ | ||||
|     private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) { | ||||
|     private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) { | ||||
|         context.notificationManager.notify(id, build()) | ||||
|     } | ||||
|  | ||||
| @@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      * those can only be dismissed by the user. | ||||
|      */ | ||||
|     fun dismiss() { | ||||
|         context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) | ||||
|         context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             setProgress(0, 0, false) | ||||
|         } | ||||
|         notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID) | ||||
|         notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) | ||||
|  | ||||
|         // Reset download information | ||||
|         errorThrown = true | ||||
|   | ||||
| @@ -122,7 +122,7 @@ class DownloadService : Service() { | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ state -> onNetworkStateChanged(state) | ||||
|                 }, { error -> | ||||
|                 }, { _ -> | ||||
|                     toast(R.string.download_queue_error) | ||||
|                     stopSelf() | ||||
|                 }) | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList | ||||
| import eu.kanade.tachiyomi.util.* | ||||
| import kotlinx.coroutines.experimental.async | ||||
| import okhttp3.Response | ||||
| import rx.Observable | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| @@ -90,12 +91,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro | ||||
|     @Volatile private var isRunning: Boolean = false | ||||
|  | ||||
|     init { | ||||
|         Observable.fromCallable { store.restore() } | ||||
|                 .map { downloads -> downloads.filter { isDownloadAllowed(it) } } | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ downloads -> queue.addAll(downloads) | ||||
|                 }, { error -> Timber.e(error) }) | ||||
|         launchNow { | ||||
|             val chapters = async { store.restore() } | ||||
|             queue.addAll(chapters.await()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -213,61 +212,54 @@ class Downloader(private val context: Context, private val provider: DownloadPro | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a download object for every chapter and adds them to the downloads queue. This method | ||||
|      * must be called in the main thread. | ||||
|      * Creates a download object for every chapter and adds them to the downloads queue. | ||||
|      * | ||||
|      * @param manga the manga of the chapters to download. | ||||
|      * @param chapters the list of chapters to download. | ||||
|      */ | ||||
|     fun queueChapters(manga: Manga, chapters: List<Chapter>) { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return | ||||
|     fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI | ||||
|  | ||||
|         val chaptersToQueue = chapters | ||||
|                 // Avoid downloading chapters with the same name. | ||||
|                 .distinctBy { it.name } | ||||
|                 // Add chapters to queue from the start. | ||||
|                 .sortedByDescending { it.source_order } | ||||
|                 // Create a downloader for each one. | ||||
|                 .map { Download(source, manga, it) } | ||||
|                 // Filter out those already queued or downloaded. | ||||
|                 .filter { isDownloadAllowed(it) } | ||||
|         // Called in background thread, the operation can be slow with SAF. | ||||
|         val chaptersWithoutDir = async { | ||||
|             val mangaDir = provider.findMangaDir(source, manga) | ||||
|  | ||||
|         // Return if there's nothing to queue. | ||||
|         if (chaptersToQueue.isEmpty()) | ||||
|             return | ||||
|  | ||||
|         queue.addAll(chaptersToQueue) | ||||
|  | ||||
|         // Initialize queue size. | ||||
|         notifier.initialQueueSize = queue.size | ||||
|  | ||||
|         // Initial multi-thread | ||||
|         notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1 | ||||
|  | ||||
|         if (isRunning) { | ||||
|             // Send the list of downloads to the downloader. | ||||
|             downloadsRelay.call(chaptersToQueue) | ||||
|         } else { | ||||
|             // Show initial notification. | ||||
|             notifier.onProgressChange(queue) | ||||
|             chapters | ||||
|                     // Avoid downloading chapters with the same name. | ||||
|                     .distinctBy { it.name } | ||||
|                     // Filter out those already downloaded. | ||||
|                     .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                     // Add chapters to queue from the start. | ||||
|                     .sortedByDescending { it.source_order } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns true if the given download can be queued and downloaded. | ||||
|      * | ||||
|      * @param download the download to be checked. | ||||
|      */ | ||||
|     private fun isDownloadAllowed(download: Download): Boolean { | ||||
|         // If the chapter is already queued, don't add it again | ||||
|         if (queue.any { it.chapter.id == download.chapter.id }) | ||||
|             return false | ||||
|         // Runs in main thread (synchronization needed). | ||||
|         val chaptersToQueue = chaptersWithoutDir.await() | ||||
|                 // Filter out those already enqueued. | ||||
|                 .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|                 // Create a download for each one. | ||||
|                 .map { Download(source, manga, it) } | ||||
|  | ||||
|         val dir = provider.findChapterDir(download.source, download.manga, download.chapter) | ||||
|         if (dir != null && dir.exists()) | ||||
|             return false | ||||
|         if (chaptersToQueue.isNotEmpty()) { | ||||
|             queue.addAll(chaptersToQueue) | ||||
|  | ||||
|         return true | ||||
|             // Initialize queue size. | ||||
|             notifier.initialQueueSize = queue.size | ||||
|  | ||||
|             // Initial multi-thread | ||||
|             notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1 | ||||
|  | ||||
|             if (isRunning) { | ||||
|                 // Send the list of downloads to the downloader. | ||||
|                 downloadsRelay.call(chaptersToQueue) | ||||
|             } else { | ||||
|                 // Show initial notification. | ||||
|                 notifier.onProgressChange(queue) | ||||
|             } | ||||
|  | ||||
|             // Start downloader if needed | ||||
|             DownloadService.start(this@Downloader.context) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -295,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro | ||||
|         } | ||||
|  | ||||
|         return pageListObservable | ||||
|                 .doOnNext { pages -> | ||||
|                 .doOnNext { _ -> | ||||
|                     // Delete all temporary (unfinished) files | ||||
|                     tmpDir.listFiles() | ||||
|                             ?.filter { it.name!!.endsWith(".tmp") } | ||||
| @@ -311,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro | ||||
|                 // Do when page is downloaded. | ||||
|                 .doOnNext { notifier.onProgressChange(download, queue) } | ||||
|                 .toList() | ||||
|                 .map { pages -> download } | ||||
|                 .map { _ -> download } | ||||
|                 // Do after download completes | ||||
|                 .doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) } | ||||
|                 // If the page list threw, it will resume here | ||||
|   | ||||
| @@ -66,6 +66,7 @@ class DownloadQueue( | ||||
|                         val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                         setPagesSubject(download.pages, pageStatusSubject) | ||||
|                         return@flatMap pageStatusSubject | ||||
|                                 .onBackpressureBuffer() | ||||
|                                 .filter { it == Page.READY } | ||||
|                                 .map { download } | ||||
|  | ||||
|   | ||||
| @@ -1,35 +1,51 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
| import java.io.InputStream | ||||
|  | ||||
| open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|  | ||||
|     private var data: InputStream? = null | ||||
|  | ||||
|     override fun loadData(priority: Priority): InputStream { | ||||
|         data = file.inputStream() | ||||
|         return data!! | ||||
|     } | ||||
|  | ||||
|     override fun cleanup() { | ||||
|         data?.let { data -> | ||||
|             try { | ||||
|                 data.close() | ||||
|             } catch (e: IOException) { | ||||
|                 // Ignore | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun cancel() { | ||||
|         // Do nothing. | ||||
|     } | ||||
|  | ||||
|     override fun getId(): String { | ||||
|         return file.toString() | ||||
|     } | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import android.content.ContentValues.TAG | ||||
| import android.util.Log | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.DataSource | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import java.io.* | ||||
|  | ||||
| open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|  | ||||
|     private var data: InputStream? = null | ||||
|  | ||||
|     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         loadFromFile(callback) | ||||
|     } | ||||
|  | ||||
|     protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         try { | ||||
|             data = FileInputStream(file) | ||||
|         } catch (e: FileNotFoundException) { | ||||
|             if (Log.isLoggable(TAG, Log.DEBUG)) { | ||||
|                 Log.d(TAG, "Failed to open file", e) | ||||
|             } | ||||
|             callback.onLoadFailed(e) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         callback.onDataReady(data) | ||||
|     } | ||||
|  | ||||
|     override fun cleanup() { | ||||
|         try { | ||||
|             data?.close() | ||||
|         } catch (e: IOException) { | ||||
|             // Ignored. | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun cancel() { | ||||
|         // Do nothing. | ||||
|     } | ||||
|  | ||||
|     override fun getDataClass(): Class<InputStream> { | ||||
|         return InputStream::class.java | ||||
|     } | ||||
|  | ||||
|     override fun getDataSource(): DataSource { | ||||
|         return DataSource.LOCAL | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,72 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import java.io.File | ||||
| import java.io.FileNotFoundException | ||||
| import java.io.InputStream | ||||
|  | ||||
| /** | ||||
|  * A [DataFetcher] for loading a cover of a library manga. | ||||
|  * It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network | ||||
|  * and copies the result to the cache. | ||||
|  * | ||||
|  * @param networkFetcher the network fetcher for this cover. | ||||
|  * @param manga the manga of the cover to load. | ||||
|  * @param file the file where this cover should be. It may exists or not. | ||||
|  */ | ||||
| class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>, | ||||
|                              private val manga: Manga, | ||||
|                              private val file: File) | ||||
| : FileFetcher(file) { | ||||
|  | ||||
|     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         if (!file.exists()) { | ||||
|             networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { | ||||
|                 override fun onDataReady(data: InputStream?) { | ||||
|                     if (data != null) { | ||||
|                         val tmpFile = File(file.path + ".tmp") | ||||
|                         try { | ||||
|                             // Retrieve destination stream, create parent folders if needed. | ||||
|                             val output = try { | ||||
|                                 tmpFile.outputStream() | ||||
|                             } catch (e: FileNotFoundException) { | ||||
|                                 tmpFile.parentFile.mkdirs() | ||||
|                                 tmpFile.outputStream() | ||||
|                             } | ||||
|  | ||||
|                             // Copy the file and rename to the original. | ||||
|                             data.use { output.use { data.copyTo(output) } } | ||||
|                             tmpFile.renameTo(file) | ||||
|                             loadFromFile(callback) | ||||
|                         } catch (e: Exception) { | ||||
|                             tmpFile.delete() | ||||
|                             callback.onLoadFailed(e) | ||||
|                         } | ||||
|                     } else { | ||||
|                         callback.onLoadFailed(Exception("Null data")) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onLoadFailed(e: Exception) { | ||||
|                     callback.onLoadFailed(e) | ||||
|                 } | ||||
|  | ||||
|             }) | ||||
|         } else { | ||||
|             loadFromFile(callback) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun cleanup() { | ||||
|         super.cleanup() | ||||
|         networkFetcher.cleanup() | ||||
|     } | ||||
|  | ||||
|     override fun cancel() { | ||||
|         super.cancel() | ||||
|         networkFetcher.cancel() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import java.io.File | ||||
|  | ||||
| open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) { | ||||
|  | ||||
|     /** | ||||
|      * Returns the id for this manga's cover. | ||||
|      * | ||||
|      * Appending the file's modified date to the url, we can force Glide to skip its memory and disk | ||||
|      * lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when | ||||
|      * the file has changed. If the file doesn't exist it will append a 0. | ||||
|      */ | ||||
|     override fun getId(): String { | ||||
|         return manga.thumbnail_url + file.lastModified() | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +1,24 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.LruCache | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import com.bumptech.glide.load.Options | ||||
| import com.bumptech.glide.load.model.* | ||||
| import com.bumptech.glide.load.model.stream.StreamModelLoader | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * A class for loading a cover associated with a [Manga] that can be present in our own cache. | ||||
|  * Coupled with [MangaUrlFetcher], this class allows to implement the following flow: | ||||
|  * Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow: | ||||
|  * | ||||
|  * - Check in RAM LRU. | ||||
|  * - Check in disk LRU. | ||||
| @@ -26,7 +27,7 @@ import java.io.InputStream | ||||
|  * | ||||
|  * @param context the application context. | ||||
|  */ | ||||
| class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
| class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
|  | ||||
|     /** | ||||
|      * Cover cache where persistent covers are stored. | ||||
| @@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|     private val sourceManager: SourceManager by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Base network loader. | ||||
|      * Default network client. | ||||
|      */ | ||||
|     private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java, | ||||
|             InputStream::class.java, context) | ||||
|     private val defaultClient = Injekt.get<NetworkHelper>().client | ||||
|  | ||||
|     /** | ||||
|      * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url | ||||
|      * and the file where it should be stored in case the manga is a favorite. | ||||
|      */ | ||||
|     private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100) | ||||
|     private val lruCache = LruCache<GlideUrl, File>(100) | ||||
|  | ||||
|     /** | ||||
|      * Map where request headers are stored for a source. | ||||
| @@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|      */ | ||||
|     class Factory : ModelLoaderFactory<Manga, InputStream> { | ||||
|  | ||||
|         override fun build(context: Context, factories: GenericLoaderFactory) | ||||
|                 = MangaModelLoader(context) | ||||
|         override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> { | ||||
|             return MangaModelLoader() | ||||
|         } | ||||
|  | ||||
|         override fun teardown() {} | ||||
|     } | ||||
|  | ||||
|     override fun handles(model: Manga): Boolean { | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a fetcher for the given manga or null if the url is empty. | ||||
|      * | ||||
| @@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|      * @param width the width of the view where the resource will be loaded. | ||||
|      * @param height the height of the view where the resource will be loaded. | ||||
|      */ | ||||
|     override fun getResourceFetcher(manga: Manga, | ||||
|                                     width: Int, | ||||
|                                     height: Int): DataFetcher<InputStream>? { | ||||
|  | ||||
|     override fun buildLoadData(manga: Manga, width: Int, height: Int, | ||||
|                                options: Options?): ModelLoader.LoadData<InputStream>? { | ||||
|         // Check thumbnail is not null or empty | ||||
|         val url = manga.thumbnail_url | ||||
|         if (url == null || url.isEmpty()) { | ||||
| @@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|  | ||||
|         if (url.startsWith("http")) { | ||||
|             val source = sourceManager.get(manga.source) as? HttpSource | ||||
|  | ||||
|             // Obtain the request url and the file for this url from the LRU cache, or calculate it | ||||
|             // and add them to the cache. | ||||
|             val (glideUrl, file) = lruCache.get(url) ?: | ||||
|                     Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply { | ||||
|                         lruCache.put(url, this) | ||||
|                     } | ||||
|             val glideUrl = GlideUrl(url, getHeaders(manga, source)) | ||||
|  | ||||
|             // Get the resource fetcher for this request url. | ||||
|             val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) } | ||||
|                 ?: baseUrlLoader.getResourceFetcher(glideUrl, width, height) | ||||
|             val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl) | ||||
|  | ||||
|             if (!manga.favorite) { | ||||
|                 return ModelLoader.LoadData(glideUrl, networkFetcher) | ||||
|             } | ||||
|  | ||||
|             // Obtain the file for this url from the LRU cache, or retrieve and add it to the cache. | ||||
|             val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) } | ||||
|  | ||||
|             val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file) | ||||
|  | ||||
|             // Return an instance of the fetcher providing the needed elements. | ||||
|             return MangaUrlFetcher(networkFetcher, file, manga) | ||||
|             return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher) | ||||
|         } else { | ||||
|             // Get the file from the url, removing the scheme if present. | ||||
|             val file = File(url.substringAfter("file://")) | ||||
|  | ||||
|             // Return an instance of the fetcher providing the needed elements. | ||||
|             return MangaFileFetcher(file, manga) | ||||
|             return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V { | ||||
|         val value = get(key) | ||||
|         return if (value == null) { | ||||
|             val answer = defaultValue() | ||||
|             put(key, answer) | ||||
|             answer | ||||
|         } else { | ||||
|             value | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import com.bumptech.glide.load.Key | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import java.io.File | ||||
| import java.security.MessageDigest | ||||
|  | ||||
| class MangaSignature(manga: Manga, file: File) : Key { | ||||
|  | ||||
|     private val key = manga.thumbnail_url + file.lastModified() | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         return if (other is MangaSignature) { | ||||
|             key == other.key | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         return key.hashCode() | ||||
|     } | ||||
|  | ||||
|     override fun updateDiskCacheKey(md: MessageDigest) { | ||||
|         md.update(key.toByteArray(Key.CHARSET)) | ||||
|     } | ||||
| } | ||||
| @@ -1,71 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import java.io.File | ||||
| import java.io.FileNotFoundException | ||||
| import java.io.InputStream | ||||
|  | ||||
| /** | ||||
|  * A [DataFetcher] for loading a cover of a manga depending on its favorite status. | ||||
|  * If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it | ||||
|  * fallbacks to network and copies it to the cache. | ||||
|  * If the manga is not favorite, it tries to delete the cover from our cache and always fallback | ||||
|  * to network for fetching. | ||||
|  * | ||||
|  * @param networkFetcher the network fetcher for this cover. | ||||
|  * @param file the file where this cover should be. It may exists or not. | ||||
|  * @param manga the manga of the cover to load. | ||||
|  */ | ||||
| class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>, | ||||
|                       private val file: File, | ||||
|                       private val manga: Manga) | ||||
| : MangaFileFetcher(file, manga) { | ||||
|  | ||||
|     override fun loadData(priority: Priority): InputStream { | ||||
|         if (manga.favorite) { | ||||
|             synchronized(file) { | ||||
|                 if (!file.exists()) { | ||||
|                     val tmpFile = File(file.path + ".tmp") | ||||
|                     try { | ||||
|                         // Retrieve source stream. | ||||
|                         val input = networkFetcher.loadData(priority) | ||||
|                                 ?: throw Exception("Couldn't open source stream") | ||||
|  | ||||
|                         // Retrieve destination stream, create parent folders if needed. | ||||
|                         val output = try { | ||||
|                             tmpFile.outputStream() | ||||
|                         } catch (e: FileNotFoundException) { | ||||
|                             tmpFile.parentFile.mkdirs() | ||||
|                             tmpFile.outputStream() | ||||
|                         } | ||||
|  | ||||
|                         // Copy the file and rename to the original. | ||||
|                         input.use { output.use { input.copyTo(output) } } | ||||
|                         tmpFile.renameTo(file) | ||||
|                     } catch (e: Exception) { | ||||
|                         tmpFile.delete() | ||||
|                         throw e | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return super.loadData(priority) | ||||
|         } else { | ||||
|             if (file.exists()) { | ||||
|                 file.delete() | ||||
|             } | ||||
|             return networkFetcher.loadData(priority) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun cancel() { | ||||
|         networkFetcher.cancel() | ||||
|     } | ||||
|  | ||||
|     override fun cleanup() { | ||||
|         super.cleanup() | ||||
|         networkFetcher.cleanup() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,12 +1,18 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.graphics.drawable.Drawable | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.GlideBuilder | ||||
| import com.bumptech.glide.Registry | ||||
| import com.bumptech.glide.annotation.GlideModule | ||||
| import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader | ||||
| import com.bumptech.glide.load.DecodeFormat | ||||
| import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory | ||||
| import com.bumptech.glide.load.model.GlideUrl | ||||
| import com.bumptech.glide.module.GlideModule | ||||
| import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions | ||||
| import com.bumptech.glide.module.AppGlideModule | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import uy.kohesive.injekt.Injekt | ||||
| @@ -16,17 +22,20 @@ import java.io.InputStream | ||||
| /** | ||||
|  * Class used to update Glide module settings | ||||
|  */ | ||||
| class AppGlideModule : GlideModule { | ||||
| @GlideModule | ||||
| class TachiGlideModule : AppGlideModule() { | ||||
| 
 | ||||
|     override fun applyOptions(context: Context, builder: GlideBuilder) { | ||||
|         // Set the cache size of Glide to 15 MiB | ||||
|         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024)) | ||||
|         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) | ||||
|         builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) | ||||
|         builder.setDefaultTransitionOptions(Drawable::class.java, | ||||
|                 DrawableTransitionOptions.withCrossFade()) | ||||
|     } | ||||
| 
 | ||||
|     override fun registerComponents(context: Context, glide: Glide) { | ||||
|     override fun registerComponents(context: Context, glide: Glide, registry: Registry) { | ||||
|         val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) | ||||
| 
 | ||||
|         glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory) | ||||
|         glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) | ||||
|         registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) | ||||
|         registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) | ||||
|     } | ||||
| } | ||||
| @@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() { | ||||
|                         .setRequiredNetworkType(wifiRestriction) | ||||
|                         .setRequiresCharging(acRestriction) | ||||
|                         .setRequirementsEnforced(true) | ||||
|                         .setPersisted(true) | ||||
|                         .setUpdateCurrent(true) | ||||
|                         .build() | ||||
|                         .schedule() | ||||
|   | ||||
| @@ -10,16 +10,17 @@ import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| import android.support.v4.app.NotificationCompat | ||||
| import eu.kanade.tachiyomi.Constants | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| @@ -80,10 +81,12 @@ class LibraryUpdateService( | ||||
|     /** | ||||
|      * Cached progress notification to avoid creating a lot. | ||||
|      */ | ||||
|     private val progressNotification by lazy { NotificationCompat.Builder(this) | ||||
|     private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) | ||||
|             .setContentTitle(getString(R.string.app_name)) | ||||
|             .setSmallIcon(R.drawable.ic_refresh_white_24dp_img) | ||||
|             .setLargeIcon(notificationBitmap) | ||||
|             .setOngoing(true) | ||||
|             .setOnlyAlertOnce(true) | ||||
|             .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) | ||||
|     } | ||||
|  | ||||
| @@ -132,7 +135,11 @@ class LibraryUpdateService( | ||||
|                     putExtra(KEY_TARGET, target) | ||||
|                     category?.let { putExtra(KEY_CATEGORY, it.id) } | ||||
|                 } | ||||
|                 context.startService(intent) | ||||
|                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { | ||||
|                     context.startService(intent) | ||||
|                 } else { | ||||
|                     context.startForegroundService(intent) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -153,6 +160,7 @@ class LibraryUpdateService( | ||||
|      */ | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") | ||||
|         wakeLock.acquire() | ||||
| @@ -224,7 +232,7 @@ class LibraryUpdateService( | ||||
|      * @param target the target to update. | ||||
|      * @return a list of manga to update | ||||
|      */ | ||||
|     fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> { | ||||
|     fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { | ||||
|         val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) | ||||
|  | ||||
|         var listToUpdate = if (categoryId != -1) | ||||
| @@ -255,7 +263,7 @@ class LibraryUpdateService( | ||||
|      * @param mangaToUpdate the list to update | ||||
|      * @return an observable delivering the progress of each update. | ||||
|      */ | ||||
|     fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> { | ||||
|     fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> { | ||||
|         // Initialize the variables holding the progress of the updates. | ||||
|         val count = AtomicInteger(0) | ||||
|         // List containing new updates | ||||
| @@ -279,7 +287,7 @@ class LibraryUpdateService( | ||||
|                             // If there's any error, return empty update and continue. | ||||
|                             .onErrorReturn { | ||||
|                                 failedUpdates.add(manga) | ||||
|                                 Pair(emptyList<Chapter>(), emptyList<Chapter>()) | ||||
|                                 Pair(emptyList(), emptyList()) | ||||
|                             } | ||||
|                             // Filter out mangas without new chapters (or failed). | ||||
|                             .filter { pair -> pair.first.isNotEmpty() } | ||||
| @@ -347,7 +355,7 @@ class LibraryUpdateService( | ||||
|      * @param mangaToUpdate the list to update | ||||
|      * @return an observable delivering the progress of each update. | ||||
|      */ | ||||
|     fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> { | ||||
|     fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> { | ||||
|         // Initialize the variables holding the progress of the updates. | ||||
|         val count = AtomicInteger(0) | ||||
|  | ||||
| @@ -358,7 +366,7 @@ class LibraryUpdateService( | ||||
|                 // Update the details of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                             ?: return@concatMap Observable.empty<Manga>() | ||||
|                             ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|  | ||||
|                     source.fetchMangaDetails(manga) | ||||
|                             .map { networkManga -> | ||||
| @@ -377,7 +385,7 @@ class LibraryUpdateService( | ||||
|      * Method that updates the metadata of the connected tracking services. It's called in a | ||||
|      * background thread, so it's safe to do heavy operations or network calls here. | ||||
|      */ | ||||
|     private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> { | ||||
|     private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> { | ||||
|         // Initialize the variables holding the progress of the updates. | ||||
|         var count = 0 | ||||
|  | ||||
| @@ -417,7 +425,7 @@ class LibraryUpdateService( | ||||
|      * @param total the total progress. | ||||
|      */ | ||||
|     private fun showProgressNotification(manga: Manga, current: Int, total: Int) { | ||||
|         notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification | ||||
|         notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification | ||||
|                 .setContentTitle(manga.title) | ||||
|                 .setProgress(total, current, false) | ||||
|                 .build()) | ||||
| @@ -434,7 +442,7 @@ class LibraryUpdateService( | ||||
|         // Append new chapters from a previous, existing notification | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|             val previousNotification = notificationManager.activeNotifications | ||||
|                     .find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID } | ||||
|                     .find { it.id == Notifications.ID_LIBRARY_RESULT } | ||||
|  | ||||
|             if (previousNotification != null) { | ||||
|                 val oldUpdates = previousNotification.notification.extras | ||||
| @@ -446,7 +454,7 @@ class LibraryUpdateService( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification { | ||||
|         notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) { | ||||
|             setSmallIcon(R.drawable.ic_book_white_24dp) | ||||
|             setLargeIcon(notificationBitmap) | ||||
|             setContentTitle(getString(R.string.notification_new_chapters)) | ||||
| @@ -466,7 +474,7 @@ class LibraryUpdateService( | ||||
|      * Cancels the progress notification. | ||||
|      */ | ||||
|     private fun cancelProgressNotification() { | ||||
|         notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) | ||||
|         notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.os.Handler | ||||
| import eu.kanade.tachiyomi.Constants | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| @@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             ACTION_RESUME_DOWNLOADS -> DownloadService.start(context) | ||||
|             // Clear the download queue | ||||
|             ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) | ||||
|             // Show message notification created | ||||
|             ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created) | ||||
|             // Launch share activity and dismiss notification | ||||
|             ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
| @@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             // Cancel library update and dismiss notification | ||||
|             ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) | ||||
|             ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) | ||||
|             // Open reader activity | ||||
|             ACTION_OPEN_CHAPTER -> { | ||||
|                 openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
| @@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|         // Called to clear downloads. | ||||
|         private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" | ||||
|  | ||||
|         // Called to notify user shortcut is created. | ||||
|         private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED" | ||||
|  | ||||
|         // Called to dismiss notification. | ||||
|         private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION" | ||||
|  | ||||
| @@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|         } | ||||
|  | ||||
|         internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent { | ||||
|             val intent = Intent(context, NotificationReceiver::class.java).apply { | ||||
|                 action = ACTION_SHORTCUT_CREATED | ||||
|             } | ||||
|             return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Returns [PendingIntent] that starts a service which dismissed the notification | ||||
|          * | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| package eu.kanade.tachiyomi.data.notification | ||||
|  | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
|  | ||||
| /** | ||||
|  * Class to manage the basic information of all the notifications used in the app. | ||||
|  */ | ||||
| object Notifications { | ||||
|  | ||||
|     /** | ||||
|      * Common notification channel and ids used anywhere. | ||||
|      */ | ||||
|     const val CHANNEL_COMMON = "common_channel" | ||||
|     const val ID_UPDATER = 1 | ||||
|     const val ID_DOWNLOAD_IMAGE = 2 | ||||
|  | ||||
|     /** | ||||
|      * Notification channel and ids used by the library updater. | ||||
|      */ | ||||
|     const val CHANNEL_LIBRARY = "library_channel" | ||||
|     const val ID_LIBRARY_PROGRESS = 101 | ||||
|     const val ID_LIBRARY_RESULT = 102 | ||||
|  | ||||
|     /** | ||||
|      * Notification channel and ids used by the downloader. | ||||
|      */ | ||||
|     const val CHANNEL_DOWNLOADER = "downloader_channel" | ||||
|     const val ID_DOWNLOAD_CHAPTER = 201 | ||||
|     const val ID_DOWNLOAD_CHAPTER_ERROR = 202 | ||||
|  | ||||
|     /** | ||||
|      * Creates the notification channels introduced in Android Oreo. | ||||
|      * | ||||
|      * @param context The application context. | ||||
|      */ | ||||
|     fun createChannels(context: Context) { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return | ||||
|  | ||||
|         val channels = listOf( | ||||
|                 NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                         NotificationManager.IMPORTANCE_LOW), | ||||
|                 NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                         NotificationManager.IMPORTANCE_LOW), | ||||
|                 NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), | ||||
|                         NotificationManager.IMPORTANCE_LOW) | ||||
|         ) | ||||
|         context.notificationManager.createNotificationChannels(channels) | ||||
|     } | ||||
| } | ||||
| @@ -105,6 +105,8 @@ object PreferenceKeys { | ||||
|  | ||||
|     const val defaultCategory = "default_category" | ||||
|  | ||||
|     const val downloadBadge = "display_download_badge" | ||||
|  | ||||
|     fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" | ||||
|  | ||||
|     fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" | ||||
|   | ||||
| @@ -142,6 +142,8 @@ class PreferencesHelper(val context: Context) { | ||||
|  | ||||
|     fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) | ||||
|  | ||||
|     fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false) | ||||
|  | ||||
|     fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false) | ||||
|  | ||||
|     fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false) | ||||
|   | ||||
| @@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat | ||||
| import com.evernote.android.job.Job | ||||
| import com.evernote.android.job.JobManager | ||||
| import com.evernote.android.job.JobRequest | ||||
| import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
|  | ||||
| class UpdateCheckerJob : Job() { | ||||
| @@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() { | ||||
|                             putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) | ||||
|                         } | ||||
|  | ||||
|                         NotificationCompat.Builder(context).update { | ||||
|                         NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { | ||||
|                             setContentTitle(context.getString(R.string.app_name)) | ||||
|                             setContentText(context.getString(R.string.update_check_notification_update_available)) | ||||
|                             setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
| @@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() { | ||||
|  | ||||
|     fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { | ||||
|         block() | ||||
|         context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) | ||||
|         context.notificationManager.notify(Notifications.ID_UPDATER, build()) | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
| @@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() { | ||||
|                     .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) | ||||
|                     .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) | ||||
|                     .setRequirementsEnforced(true) | ||||
|                     .setPersisted(true) | ||||
|                     .setUpdateCurrent(true) | ||||
|                     .build() | ||||
|                     .schedule() | ||||
|   | ||||
| @@ -4,10 +4,10 @@ import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.support.v4.app.NotificationCompat | ||||
| import eu.kanade.tachiyomi.Constants | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationHandler | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
| import java.io.File | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
| @@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|     /** | ||||
|      * Notification shown to user | ||||
|      */ | ||||
|     private val notification = NotificationCompat.Builder(context) | ||||
|     private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON) | ||||
|  | ||||
|     override fun onReceive(context: Context, intent: Intent) { | ||||
|         when (intent.getStringExtra(EXTRA_ACTION)) { | ||||
| @@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|     private fun updateProgress(progress: Int) { | ||||
|         with(notification) { | ||||
|             setProgress(100, progress, false) | ||||
|             setOnlyAlertOnce(true) | ||||
|         } | ||||
|         notification.show() | ||||
|     } | ||||
| @@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|         with(notification) { | ||||
|             setContentText(context.getString(R.string.update_check_notification_download_complete)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
|             setOnlyAlertOnce(false) | ||||
|             setProgress(0, 0, false) | ||||
|             // Install action | ||||
|             setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) | ||||
| @@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_clear_grey_24dp_img, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|         } | ||||
|         notification.show() | ||||
|     } | ||||
| @@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|         with(notification) { | ||||
|             setContentText(context.getString(R.string.update_check_notification_download_error)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_warning) | ||||
|             setOnlyAlertOnce(false) | ||||
|             setProgress(0, 0, false) | ||||
|             // Retry action | ||||
|             addAction(R.drawable.ic_refresh_grey_24dp_img, | ||||
| @@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_clear_grey_24dp_img, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|         } | ||||
|         notification.show() | ||||
|     } | ||||
| @@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive | ||||
|      * | ||||
|      * @param id the id of the notification. | ||||
|      */ | ||||
|     private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) { | ||||
|     private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) { | ||||
|         context.notificationManager.notify(id, build()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) { | ||||
|             val cookies = value as? Set<String> | ||||
|             if (cookies != null) { | ||||
|                 try { | ||||
|                     val url = HttpUrl.parse("http://$key") | ||||
|                     val url = HttpUrl.parse("http://$key") ?: continue | ||||
|                     val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } | ||||
|                             .filter { !it.hasExpired() } | ||||
|                     cookieMap.put(key, nonExpiredCookies) | ||||
|   | ||||
| @@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|     } | ||||
|  | ||||
|     override val id = ID | ||||
|     override val name = "LocalSource" | ||||
|     override val name = context.getString(R.string.local_source) | ||||
|     override val lang = "" | ||||
|     override val supportsLatest = true | ||||
|  | ||||
| @@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|         val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state | ||||
|         when (state?.index) { | ||||
|             0 -> { | ||||
|                 if (state!!.ascending) | ||||
|                 if (state.ascending) | ||||
|                     mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } | ||||
|                 else | ||||
|                     mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } | ||||
|             } | ||||
|             1 -> { | ||||
|                 if (state!!.ascending) | ||||
|                 if (state.ascending) | ||||
|                     mangaDirs = mangaDirs.sortedBy(File::lastModified) | ||||
|                 else | ||||
|                     mangaDirs = mangaDirs.sortedByDescending(File::lastModified) | ||||
| @@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|                         } else { | ||||
|                             chapterFile.nameWithoutExtension | ||||
|                         } | ||||
|                         val chapNameCut = chapName.replace(manga.title, "", true).trim() | ||||
|                         val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_') | ||||
|                         name = if (chapNameCut.isEmpty()) chapName else chapNameCut | ||||
|                         date_upload = chapterFile.lastModified() | ||||
|                         ChapterRecognition.parseChapterNumber(this, manga) | ||||
|   | ||||
| @@ -28,7 +28,11 @@ class Page( | ||||
|     @Transient private var statusSubject: Subject<Int, Int>? = null | ||||
|  | ||||
|     override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { | ||||
|         progress = (100 * bytesRead / contentLength).toInt() | ||||
|         progress = if (contentLength > 0) { | ||||
|             (100 * bytesRead / contentLength).toInt() | ||||
|         } else { | ||||
|             -1 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun setStatusSubject(subject: Subject<Int, Int>?) { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.lang.Exception | ||||
| import java.net.URI | ||||
| import java.net.URISyntaxException | ||||
| import java.security.MessageDigest | ||||
| @@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource { | ||||
|     override val id by lazy { | ||||
|         val key = "${name.toLowerCase()}/$lang/$versionId" | ||||
|         val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) | ||||
|         (0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE | ||||
|         (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource { | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable with the updated chapter list for a manga. Normally it's not needed to | ||||
|      * override this method. | ||||
|      * override this method.  If a manga is licensed an empty chapter list observable is returned | ||||
|      * | ||||
|      * @param manga the manga to look for chapters. | ||||
|      */ | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         return client.newCall(chapterListRequest(manga)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     chapterListParse(response) | ||||
|                 } | ||||
|         if (manga.status != SManga.LICENSED) { | ||||
|             return client.newCall(chapterListRequest(manga)) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { response -> | ||||
|                         chapterListParse(response) | ||||
|                     } | ||||
|         } else { | ||||
|             return Observable.error(Exception("Licensed - No chapters to show")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import timber.log.Timber | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.regex.Pattern | ||||
|  | ||||
| @@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() { | ||||
|         val manga = SManga.create() | ||||
|         element.select("td a:eq(0)").first().let { | ||||
|             manga.setUrlWithoutDomain(it.attr("href")) | ||||
|             manga.title = it.text() | ||||
|             val title = it.text() | ||||
|             //check if cloudfire email obfuscation is affecting title name | ||||
|             if (title.contains("[email protected]", true)) { | ||||
|                 try { | ||||
|                     var str: String = it.html() | ||||
|                     //get the  number | ||||
|                     str = str.substringAfter("data-cfemail=\"") | ||||
|                     str = str.substringBefore("\">[email") | ||||
|                     val sb = StringBuilder() | ||||
|                     //convert number to char | ||||
|                     val r = Integer.valueOf(str.substring(0, 2), 16)!! | ||||
|                     var i = 2 | ||||
|                     while (i < str.length) { | ||||
|                         val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar() | ||||
|                         sb.append(c) | ||||
|                         i += 2 | ||||
|                     } | ||||
|                     //replace the new word into the title | ||||
|                     manga.title = title.replace("[email protected]", sb.toString(), true) | ||||
|                 } catch (e: Exception) { | ||||
|                     //on error just default to obfuscated title | ||||
|                     Timber.e("error parsing [email protected]", e) | ||||
|                     manga.title = title | ||||
|                 } | ||||
|             } else { | ||||
|                 manga.title = title | ||||
|             } | ||||
|         } | ||||
|         return manga | ||||
|     } | ||||
| @@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() { | ||||
|             Genre("Mystery"), | ||||
|             Genre("One shot"), | ||||
|             Genre("Psychological"), | ||||
|             Genre("Reincarnation"), | ||||
|             Genre("Romance"), | ||||
|             Genre("School Life"), | ||||
|             Genre("Sci-fi"), | ||||
| @@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() { | ||||
|             Genre("Smut"), | ||||
|             Genre("Sports"), | ||||
|             Genre("Supernatural"), | ||||
|             Genre("Time Travel"), | ||||
|             Genre("Tragedy"), | ||||
|             Genre("Transported"), | ||||
|             Genre("Webtoon"), | ||||
|             Genre("Yaoi"), | ||||
|             Genre("Yuri") | ||||
|   | ||||
| @@ -61,7 +61,7 @@ class Mangafox : ParsedHttpSource() { | ||||
|                 is Status -> url.addQueryParameter(filter.id, filter.state.toString()) | ||||
|                 is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } | ||||
|                 is TextField -> url.addQueryParameter(filter.key, filter.state) | ||||
|                 is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString()) | ||||
|                 is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString()) | ||||
|                 is OrderBy -> { | ||||
|                     url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) | ||||
|                     url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") | ||||
| @@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() { | ||||
|         val infoElement = document.select("div#title").first() | ||||
|         val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() | ||||
|         val sideInfoElement = document.select("#series_info").first() | ||||
|         val licensedElement = document.select("div.warning").first() | ||||
|  | ||||
|         val manga = SManga.create() | ||||
|         manga.author = rowElement.select("td:eq(1)").first()?.text() | ||||
|         manga.artist = rowElement.select("td:eq(2)").first()?.text() | ||||
|         manga.genre = rowElement.select("td:eq(3)").first()?.text() | ||||
|         manga.description = infoElement.select("p.summary").first()?.text() | ||||
|         manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } | ||||
|         val isLicensed = licensedElement?.text()?.contains("licensed") | ||||
|         if (isLicensed == true) { | ||||
|             manga.status = SManga.LICENSED | ||||
|         } else { | ||||
|             manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } | ||||
|         } | ||||
|  | ||||
|         manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") | ||||
|         return manga | ||||
|     } | ||||
| @@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() { | ||||
|  | ||||
|         val chapter = SChapter.create() | ||||
|         chapter.setUrlWithoutDomain(urlElement.attr("href")) | ||||
|         chapter.name = urlElement.text() | ||||
|         chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text() | ||||
|         chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 | ||||
|         return chapter | ||||
|     } | ||||
| @@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() { | ||||
|     private class OrderBy : Filter.Sort("Order by", | ||||
|             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), | ||||
|             Filter.Sort.Selection(2, false)) | ||||
|  | ||||
|     private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) | ||||
|  | ||||
|     override fun getFilterList() = FilterList( | ||||
|   | ||||
| @@ -7,9 +7,13 @@ import okhttp3.HttpUrl | ||||
| import okhttp3.Request | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import java.security.SecureRandom | ||||
| import java.security.cert.X509Certificate | ||||
| import java.text.ParseException | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.* | ||||
| import javax.net.ssl.SSLContext | ||||
| import javax.net.ssl.X509TrustManager | ||||
|  | ||||
| class Mangahere : ParsedHttpSource() { | ||||
|  | ||||
| @@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() { | ||||
|  | ||||
|     override val supportsLatest = true | ||||
|  | ||||
|     private val trustManager = object : X509TrustManager { | ||||
|         override fun getAcceptedIssuers(): Array<X509Certificate> { | ||||
|             return emptyArray() | ||||
|         } | ||||
|  | ||||
|         override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) { | ||||
|         } | ||||
|  | ||||
|         override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private val sslContext = SSLContext.getInstance("SSL").apply { | ||||
|         init(null, arrayOf(trustManager), SecureRandom()) | ||||
|     } | ||||
|  | ||||
|     override val client = super.client.newBuilder() | ||||
|             .sslSocketFactory(sslContext.socketFactory, trustManager) | ||||
|             .build() | ||||
|  | ||||
|     override fun popularMangaSelector() = "div.directory_list > ul > li" | ||||
|  | ||||
|     override fun latestUpdatesSelector() = "div.directory_list > ul > li" | ||||
| @@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() { | ||||
|         val infoElement = detailElement.select(".detail_topText").first() | ||||
|  | ||||
|         val manga = SManga.create() | ||||
|         manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() | ||||
|         manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() | ||||
|         manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text() | ||||
|         manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text() | ||||
|         manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") | ||||
|         manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") | ||||
|         manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } | ||||
| @@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() { | ||||
|  | ||||
|         val pages = mutableListOf<Page>() | ||||
|         document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { | ||||
|             pages.add(Page(pages.size, it.attr("value"))) | ||||
|             if (!it.attr("value").contains("featured.html")) { | ||||
|                 pages.add(Page(pages.size, "http:" + it.attr("value"))) | ||||
|             } | ||||
|         } | ||||
|         pages.getOrNull(0)?.imageUrl = imageUrlParse(document) | ||||
|         return pages | ||||
| @@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() { | ||||
|     private class OrderBy : Filter.Sort("Order by", | ||||
|             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), | ||||
|             Filter.Sort.Selection(2, false)) | ||||
|  | ||||
|     private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) | ||||
|  | ||||
|     override fun getFilterList() = FilterList( | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() { | ||||
|  | ||||
|     override val name = "ReadMangaToday" | ||||
|  | ||||
|     override val baseUrl = "http://www.readmanga.today" | ||||
|     override val baseUrl = "http://www.readmng.com/" | ||||
|  | ||||
|     override val lang = "en" | ||||
|  | ||||
| @@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() { | ||||
|         return pages | ||||
|     } | ||||
|  | ||||
|     override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") | ||||
|     override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src") | ||||
|  | ||||
|     private class Status : Filter.TriState("Completed") | ||||
|     private class Genre(name: String, val id: Int) : Filter.TriState(name) | ||||
|   | ||||
| @@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() { | ||||
|  | ||||
|     override val supportsLatest = true | ||||
|  | ||||
|     override fun popularMangaRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) | ||||
|     } | ||||
|     override fun popularMangaRequest(page: Int): Request = | ||||
|             GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) | ||||
|  | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         var pageNum = 1 | ||||
| @@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() { | ||||
|         return GET(url, headers) | ||||
|     } | ||||
|  | ||||
|     override fun latestUpdatesRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/newestch?page=$page") | ||||
|     } | ||||
|     override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page") | ||||
|  | ||||
|     override fun popularMangaSelector() = "div.content_row" | ||||
|  | ||||
| @@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() { | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     override fun searchMangaFromElement(element: Element): SManga { | ||||
|         return popularMangaFromElement(element) | ||||
|     } | ||||
|     override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) | ||||
|  | ||||
|     override fun popularMangaNextPageSelector() = "a:contains(Вперед)" | ||||
|  | ||||
| @@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() { | ||||
|         manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() | ||||
|         manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) | ||||
|         manga.description = descElement.textNodes().first().text() | ||||
|         manga.thumbnail_url = baseUrl + imgElement.attr("src") | ||||
|         manga.thumbnail_url = imgElement.attr("src") | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     private fun parseStatus(element: String): Int { | ||||
|         when { | ||||
|             element.contains("перевод завершен") -> return SManga.COMPLETED | ||||
|             element.contains("перевод продолжается") -> return SManga.ONGOING | ||||
|             else -> return SManga.UNKNOWN | ||||
|         } | ||||
|     private fun parseStatus(element: String): Int = when { | ||||
|         element.contains("перевод завершен") -> SManga.COMPLETED | ||||
|         element.contains("перевод продолжается") -> SManga.ONGOING | ||||
|         else -> SManga.UNKNOWN | ||||
|     } | ||||
|  | ||||
|     override fun chapterListSelector() = "table.table_cha tr:gt(1)" | ||||
|   | ||||
| @@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() { | ||||
|  | ||||
|     override val supportsLatest = true | ||||
|  | ||||
|     override fun popularMangaRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) | ||||
|     } | ||||
|     override fun popularMangaRequest(page: Int): Request = | ||||
|             GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) | ||||
|  | ||||
|     override fun latestUpdatesRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) | ||||
|     } | ||||
|     override fun latestUpdatesRequest(page: Int): Request = | ||||
|             GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) | ||||
|  | ||||
|     override fun popularMangaSelector() = "div.desc" | ||||
|  | ||||
| @@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() { | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     override fun latestUpdatesFromElement(element: Element): SManga { | ||||
|         return popularMangaFromElement(element) | ||||
|     } | ||||
|     override fun latestUpdatesFromElement(element: Element): SManga = | ||||
|             popularMangaFromElement(element) | ||||
|  | ||||
|     override fun popularMangaNextPageSelector() = "a.nextLink" | ||||
|  | ||||
|     override fun latestUpdatesNextPageSelector() = "a.nextLink" | ||||
|  | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") | ||||
|         return GET("$baseUrl/search?q=$query&$genres", headers) | ||||
|         val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] } | ||||
|         return GET("$baseUrl/search/advanced?q=$query&$genres", headers) | ||||
|     } | ||||
|  | ||||
|     override fun searchMangaSelector() = popularMangaSelector() | ||||
|  | ||||
|     override fun searchMangaFromElement(element: Element): SManga { | ||||
|         return popularMangaFromElement(element) | ||||
|     } | ||||
|     override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) | ||||
|  | ||||
|     // max 200 results | ||||
|     override fun searchMangaNextPageSelector() = null | ||||
| @@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() { | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     private fun parseStatus(element: String): Int { | ||||
|         when { | ||||
|             element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING | ||||
|             else -> return SManga.UNKNOWN | ||||
|         } | ||||
|     private fun parseStatus(element: String): Int = when { | ||||
|         element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED | ||||
|         element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED | ||||
|         element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING | ||||
|         else -> SManga.UNKNOWN | ||||
|     } | ||||
|  | ||||
|     override fun chapterListSelector() = "div.chapters-link tbody tr" | ||||
| @@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() { | ||||
|     /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { | ||||
|     *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); | ||||
|     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') | ||||
|     *  on http://mintmanga.com/search | ||||
|     *  on http://mintmanga.com/search/advanced | ||||
|     */ | ||||
|     override fun getFilterList() = FilterList( | ||||
|             Genre("арт", "el_2220"), | ||||
| @@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() { | ||||
|             Genre("меха", "el_1318"), | ||||
|             Genre("мистика", "el_1324"), | ||||
|             Genre("научная фантастика", "el_1325"), | ||||
|             Genre("омегаверс", "el_5676"), | ||||
|             Genre("повседневность", "el_1327"), | ||||
|             Genre("постапокалиптика", "el_1342"), | ||||
|             Genre("приключения", "el_1322"), | ||||
|   | ||||
| @@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() { | ||||
|  | ||||
|     override fun latestUpdatesSelector() = "div.desc" | ||||
|  | ||||
|     override fun popularMangaRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) | ||||
|     } | ||||
|     override fun popularMangaRequest(page: Int): Request = | ||||
|             GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) | ||||
|  | ||||
|     override fun latestUpdatesRequest(page: Int): Request { | ||||
|         return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) | ||||
|     } | ||||
|     override fun latestUpdatesRequest(page: Int): Request = | ||||
|             GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) | ||||
|  | ||||
|     override fun popularMangaFromElement(element: Element): SManga { | ||||
|         val manga = SManga.create() | ||||
| @@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() { | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     override fun latestUpdatesFromElement(element: Element): SManga { | ||||
|         return popularMangaFromElement(element) | ||||
|     } | ||||
|     override fun latestUpdatesFromElement(element: Element): SManga = | ||||
|             popularMangaFromElement(element) | ||||
|  | ||||
|     override fun popularMangaNextPageSelector() = "a.nextLink" | ||||
|  | ||||
|     override fun latestUpdatesNextPageSelector() = "a.nextLink" | ||||
|  | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") | ||||
|         return GET("$baseUrl/search?q=$query&$genres", headers) | ||||
|         val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] } | ||||
|         return GET("$baseUrl/search/advanced?q=$query&$genres", headers) | ||||
|     } | ||||
|  | ||||
|     override fun searchMangaSelector() = popularMangaSelector() | ||||
|  | ||||
|     override fun searchMangaFromElement(element: Element): SManga { | ||||
|         return popularMangaFromElement(element) | ||||
|     } | ||||
|     override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) | ||||
|  | ||||
|     // max 200 results | ||||
|     override fun searchMangaNextPageSelector() = null | ||||
| @@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() { | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|     private fun parseStatus(element: String): Int { | ||||
|         when { | ||||
|             element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING | ||||
|             else -> return SManga.UNKNOWN | ||||
|         } | ||||
|     private fun parseStatus(element: String): Int = when { | ||||
|         element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED | ||||
|         element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED | ||||
|         element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING | ||||
|         else -> SManga.UNKNOWN | ||||
|     } | ||||
|  | ||||
|     override fun chapterListSelector() = "div.chapters-link tbody tr" | ||||
| @@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() { | ||||
|     /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { | ||||
|     *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); | ||||
|     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') | ||||
|     *  on http://readmanga.me/search | ||||
|     *  on http://readmanga.me/search/advanced | ||||
|     */ | ||||
|     override fun getFilterList() = FilterList( | ||||
|             Genre("арт", "el_5685"), | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.base.controller | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.support.v4.view.MenuItemCompat | ||||
| import android.support.v7.app.AppCompatActivity | ||||
| import android.view.LayoutInflater | ||||
| import android.view.MenuItem | ||||
| @@ -34,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr | ||||
|         return null | ||||
|     } | ||||
|  | ||||
|     private fun setTitle() { | ||||
|     fun setTitle() { | ||||
|         var parentController = parentController | ||||
|         while (parentController != null) { | ||||
|             if (parentController is BaseController && parentController.getTitle() != null) { | ||||
| @@ -52,7 +51,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr | ||||
|      * Issue link: https://issuetracker.google.com/issues/37657375 | ||||
|      */ | ||||
|     fun MenuItem.fixExpand() { | ||||
|         val expandListener = object : MenuItemCompat.OnActionExpandListener { | ||||
|         setOnActionExpandListener(object : MenuItem.OnActionExpandListener { | ||||
|             override fun onMenuItemActionExpand(item: MenuItem): Boolean { | ||||
|                 return true | ||||
|             } | ||||
| @@ -61,8 +60,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|         MenuItemCompat.setOnActionExpandListener(this, expandListener) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory | ||||
| import nucleus.presenter.Presenter | ||||
|  | ||||
| @Suppress("LeakingThis") | ||||
| abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(), | ||||
| abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle), | ||||
|         PresenterFactory<P> { | ||||
|  | ||||
|     private val delegate = NucleusConductorDelegate(this) | ||||
|   | ||||
| @@ -10,7 +10,6 @@ public class NucleusConductorDelegate<P extends Presenter> { | ||||
|  | ||||
|     @Nullable private P presenter; | ||||
|     @Nullable private Bundle bundle; | ||||
|     private boolean presenterHasView = false; | ||||
|  | ||||
|     private PresenterFactory<P> factory; | ||||
|  | ||||
| @@ -22,8 +21,8 @@ public class NucleusConductorDelegate<P extends Presenter> { | ||||
|         if (presenter == null) { | ||||
|             presenter = factory.createPresenter(); | ||||
|             presenter.create(bundle); | ||||
|             bundle = null; | ||||
|         } | ||||
|         bundle = null; | ||||
|         return presenter; | ||||
|     } | ||||
|  | ||||
| @@ -37,31 +36,26 @@ public class NucleusConductorDelegate<P extends Presenter> { | ||||
|     } | ||||
|  | ||||
|     void onRestoreInstanceState(Bundle presenterState) { | ||||
|         if (presenter != null) | ||||
|             throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()"); | ||||
|         bundle = presenterState; | ||||
|     } | ||||
|  | ||||
|     void onTakeView(Object view) { | ||||
|         getPresenter(); | ||||
|         if (presenter != null && !presenterHasView) { | ||||
|         if (presenter != null) { | ||||
|             //noinspection unchecked | ||||
|             presenter.takeView(view); | ||||
|             presenterHasView = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void onDropView() { | ||||
|         if (presenter != null && presenterHasView) { | ||||
|         if (presenter != null) { | ||||
|             presenter.dropView(); | ||||
|             presenterHasView = false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void onDestroy() { | ||||
|         if (presenter != null) { | ||||
|             presenter.destroy(); | ||||
|             presenter = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,24 +4,20 @@ import android.content.res.Configuration | ||||
| import android.os.Bundle | ||||
| import android.support.design.widget.Snackbar | ||||
| import android.support.v4.widget.DrawerLayout | ||||
| import android.support.v7.app.AppCompatActivity | ||||
| import android.support.v7.widget.* | ||||
| import android.view.* | ||||
| import android.widget.AdapterView | ||||
| import android.widget.ArrayAdapter | ||||
| import android.widget.Spinner | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.bluelinelabs.conductor.RouterTransaction | ||||
| import com.bluelinelabs.conductor.changehandler.FadeChangeHandler | ||||
| import com.f2prateek.rx.preferences.Preference | ||||
| import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents | ||||
| import com.jakewharton.rxbinding.widget.itemSelections | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController | ||||
| @@ -43,14 +39,18 @@ import java.util.concurrent.TimeUnit | ||||
| /** | ||||
|  * Controller to manage the catalogues available in the app. | ||||
|  */ | ||||
| open class CatalogueController(bundle: Bundle? = null) : | ||||
| open class CatalogueController(bundle: Bundle) : | ||||
|         NucleusController<CataloguePresenter>(bundle), | ||||
|         SecondaryDrawerController, | ||||
|         FlexibleAdapter.OnItemClickListener, | ||||
|         FlexibleAdapter.OnItemLongClickListener, | ||||
|         FlexibleAdapter.EndlessScrollListener<ProgressItem>, | ||||
|         FlexibleAdapter.EndlessScrollListener, | ||||
|         ChangeMangaCategoriesDialog.Listener { | ||||
|  | ||||
|     constructor(source: CatalogueSource) : this(Bundle().apply { | ||||
|         putLong(SOURCE_ID_KEY, source.id) | ||||
|     }) | ||||
|  | ||||
|     /** | ||||
|      * Preferences helper. | ||||
|      */ | ||||
| @@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|      */ | ||||
|     private var adapter: FlexibleAdapter<IFlexible<*>>? = null | ||||
|  | ||||
|     /** | ||||
|      * Spinner shown in the toolbar to change the selected source. | ||||
|      */ | ||||
|     private var spinner: Spinner? = null | ||||
|  | ||||
|     /** | ||||
|      * Snackbar containing an error message when a request fails. | ||||
|      */ | ||||
| @@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|      */ | ||||
|     private var recycler: RecyclerView? = null | ||||
|  | ||||
|     /** | ||||
|      * Drawer listener to allow swipe only for closing the drawer. | ||||
|      */ | ||||
|     private var drawerListener: DrawerLayout.DrawerListener? = null | ||||
|  | ||||
|     /** | ||||
|      * Query of the search box. | ||||
|      */ | ||||
|     private val query: String | ||||
|         get() = presenter.query | ||||
|  | ||||
|     /** | ||||
|      * Selected index of the spinner (selected source). | ||||
|      */ | ||||
|     private var selectedIndex: Int = 0 | ||||
|  | ||||
|     /** | ||||
|      * Subscription for the search view. | ||||
|      */ | ||||
|     private var searchViewSubscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Subscription for the number of manga per row. | ||||
|      */ | ||||
|     private var numColumnsSubscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Endless loading item. | ||||
|      */ | ||||
|     private var progressItem: ProgressItem? = null | ||||
|  | ||||
|     init { | ||||
| @@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|     } | ||||
|  | ||||
|     override fun getTitle(): String? { | ||||
|         return "" | ||||
|         return presenter.source.name | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter(): CataloguePresenter { | ||||
|         return CataloguePresenter() | ||||
|         return CataloguePresenter(args.getLong(SOURCE_ID_KEY)) | ||||
|     } | ||||
|  | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { | ||||
| @@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|         adapter = FlexibleAdapter(null, this) | ||||
|         setupRecycler(view) | ||||
|  | ||||
|         // Create toolbar spinner | ||||
|         val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext | ||||
|                 ?: activity | ||||
|  | ||||
|         val spinnerAdapter = ArrayAdapter(themedContext, | ||||
|                 android.R.layout.simple_spinner_item, presenter.sources) | ||||
|         spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item) | ||||
|  | ||||
|         val onItemSelected: (Int) -> Unit = { position -> | ||||
|             val source = spinnerAdapter.getItem(position) | ||||
|             if (!presenter.isValidSource(source)) { | ||||
|                 spinner?.setSelection(selectedIndex) | ||||
|                 activity?.toast(R.string.source_requires_login) | ||||
|             } else if (source != presenter.source) { | ||||
|                 selectedIndex = position | ||||
|                 showProgressBar() | ||||
|                 adapter?.clear() | ||||
|                 presenter.setActiveSource(source) | ||||
|                 navView?.setFilters(presenter.filterItems) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         selectedIndex = presenter.sources.indexOf(presenter.source) | ||||
|  | ||||
|         spinner = Spinner(themedContext).apply { | ||||
|             adapter = spinnerAdapter | ||||
|             setSelection(selectedIndex) | ||||
|             itemSelections() | ||||
|                     .skip(1) | ||||
|                     .filter { it != AdapterView.INVALID_POSITION } | ||||
|                     .subscribeUntilDestroy { onItemSelected(it) } | ||||
|         } | ||||
|  | ||||
|         activity?.toolbar?.addView(spinner) | ||||
|         navView?.setFilters(presenter.filterItems) | ||||
|  | ||||
|         view.progress?.visible() | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         activity?.toolbar?.removeView(spinner) | ||||
|         numColumnsSubscription?.unsubscribe() | ||||
|         numColumnsSubscription = null | ||||
|         searchViewSubscription?.unsubscribe() | ||||
|         searchViewSubscription = null | ||||
|         adapter = null | ||||
|         spinner = null | ||||
|         snack = null | ||||
|         recycler = null | ||||
|     } | ||||
| @@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|         } | ||||
|         navView.setFilters(presenter.filterItems) | ||||
|  | ||||
|         navView.post { | ||||
|             if (isAttached && !drawer.isDrawerOpen(navView)) | ||||
|                 drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) | ||||
|         } | ||||
|         drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) | ||||
|  | ||||
|         navView.onSearchClicked = { | ||||
|             val allDefault = presenter.sourceFilters == presenter.source.getFilterList() | ||||
| @@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|  | ||||
|         val recycler = if (presenter.isListMode) { | ||||
|             RecyclerView(view.context).apply { | ||||
|                 id = R.id.recycler | ||||
|                 layoutManager = LinearLayoutManager(context) | ||||
|                 addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) | ||||
|             } | ||||
| @@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|         menu.findItem(R.id.action_search).apply { | ||||
|             val searchView = actionView as SearchView | ||||
|  | ||||
|             val query = presenter.query | ||||
|             if (!query.isBlank()) { | ||||
|                 expandActionView() | ||||
|                 searchView.setQuery(query, true) | ||||
| @@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|      */ | ||||
|     private fun searchWithQuery(newQuery: String) { | ||||
|         // If text didn't change, do nothing | ||||
|         if (query == newQuery) | ||||
|         if (presenter.query == newQuery) | ||||
|             return | ||||
|  | ||||
|         // FIXME dirty fix to restore the toolbar buttons after closing search mode. | ||||
|         if (newQuery == "") { | ||||
|             activity?.invalidateOptionsMenu() | ||||
|         } | ||||
|  | ||||
|         showProgressBar() | ||||
|         adapter?.clear() | ||||
|  | ||||
| @@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|      */ | ||||
|     fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { | ||||
|         return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) | ||||
|             presenter.prefs.portraitColumns() | ||||
|             preferences.portraitColumns() | ||||
|         else | ||||
|             presenter.prefs.landscapeColumns() | ||||
|             preferences.landscapeColumns() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) : | ||||
|         presenter.updateMangaCategories(manga, categories) | ||||
|     } | ||||
|  | ||||
|     protected companion object { | ||||
|         const val SOURCE_ID_KEY = "sourceId" | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue | ||||
|  | ||||
| import android.view.View | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.widget.StateImageViewTarget | ||||
| import kotlinx.android.synthetic.main.catalogue_grid_item.view.* | ||||
|  | ||||
| @@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA | ||||
|     } | ||||
|  | ||||
|     override fun setImage(manga: Manga) { | ||||
|         Glide.clear(view.thumbnail) | ||||
|         GlideApp.with(view.context).clear(view.thumbnail) | ||||
|         if (!manga.thumbnail_url.isNullOrEmpty()) { | ||||
|             Glide.with(view.context) | ||||
|             GlideApp.with(view.context) | ||||
|                     .load(manga) | ||||
|                     .diskCacheStrategy(DiskCacheStrategy.SOURCE) | ||||
|                     .diskCacheStrategy(DiskCacheStrategy.DATA) | ||||
|                     .centerCrop() | ||||
|                     .skipMemoryCache(true) | ||||
|                     .placeholder(android.R.color.transparent) | ||||
|                     .into(StateImageViewTarget(view.thumbnail, view.progress)) | ||||
|  | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,39 +1,40 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue | ||||
|  | ||||
| import android.view.Gravity | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.view.View | ||||
| import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| import android.widget.FrameLayout | ||||
| import com.f2prateek.rx.preferences.Preference | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.util.inflate | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.widget.AutofitRecyclerView | ||||
| import kotlinx.android.synthetic.main.catalogue_grid_item.view.* | ||||
|  | ||||
| class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() { | ||||
| class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Boolean>) : | ||||
|         AbstractFlexibleItem<CatalogueHolder>() { | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_grid_item | ||||
|         return if (catalogueAsList.getOrDefault()) | ||||
|             R.layout.catalogue_list_item | ||||
|         else | ||||
|             R.layout.catalogue_grid_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, | ||||
|                                   inflater: LayoutInflater, | ||||
|                                   parent: ViewGroup): CatalogueHolder { | ||||
|  | ||||
|         if (parent is AutofitRecyclerView) { | ||||
|             val view = parent.inflate(R.layout.catalogue_grid_item).apply { | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder { | ||||
|         val parent = adapter.recyclerView | ||||
|         return if (parent is AutofitRecyclerView) { | ||||
|             view.apply { | ||||
|                 card.layoutParams = FrameLayout.LayoutParams( | ||||
|                         MATCH_PARENT, parent.itemWidth / 3 * 4) | ||||
|                 gradient.layoutParams = FrameLayout.LayoutParams( | ||||
|                         MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM) | ||||
|             } | ||||
|             return CatalogueGridHolder(view, adapter) | ||||
|             CatalogueGridHolder(view, adapter) | ||||
|         } else { | ||||
|             val view = parent.inflate(R.layout.catalogue_list_item) | ||||
|             return CatalogueListHolder(view, adapter) | ||||
|             CatalogueListHolder(view, adapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue | ||||
|  | ||||
| import android.view.View | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.util.getResourceColor | ||||
| import jp.wasabeef.glide.transformations.CropCircleTransformation | ||||
| import kotlinx.android.synthetic.main.catalogue_list_item.view.* | ||||
|  | ||||
| /** | ||||
| @@ -37,13 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) : | ||||
|     } | ||||
|  | ||||
|     override fun setImage(manga: Manga) { | ||||
|         Glide.clear(view.thumbnail) | ||||
|         GlideApp.with(view.context).clear(view.thumbnail) | ||||
|         if (!manga.thumbnail_url.isNullOrEmpty()) { | ||||
|             Glide.with(view.context) | ||||
|             GlideApp.with(view.context) | ||||
|                     .load(manga) | ||||
|                     .diskCacheStrategy(DiskCacheStrategy.SOURCE) | ||||
|                     .diskCacheStrategy(DiskCacheStrategy.DATA) | ||||
|                     .centerCrop() | ||||
|                     .bitmapTransform(CropCircleTransformation(view.context)) | ||||
|                     .circleCrop() | ||||
|                     .dontAnimate() | ||||
|                     .skipMemoryCache(true) | ||||
|                     .placeholder(android.R.color.transparent) | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: | ||||
|     } | ||||
|  | ||||
|     fun setFilters(items: List<IFlexible<*>>) { | ||||
|         adapter.updateDataSet(items.toMutableList()) | ||||
|         adapter.updateDataSet(items) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaCategory | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.source.online.LoginSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.catalogue.filter.* | ||||
| import rx.Observable | ||||
| @@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get | ||||
|  * Presenter of [CatalogueController]. | ||||
|  */ | ||||
| open class CataloguePresenter( | ||||
|         val sourceManager: SourceManager = Injekt.get(), | ||||
|         val db: DatabaseHelper = Injekt.get(), | ||||
|         val prefs: PreferencesHelper = Injekt.get(), | ||||
|         val coverCache: CoverCache = Injekt.get() | ||||
|         sourceId: Long, | ||||
|         sourceManager: SourceManager = Injekt.get(), | ||||
|         private val db: DatabaseHelper = Injekt.get(), | ||||
|         private val prefs: PreferencesHelper = Injekt.get(), | ||||
|         private val coverCache: CoverCache = Injekt.get() | ||||
| ) : BasePresenter<CatalogueController>() { | ||||
|  | ||||
|     /** | ||||
|      * Enabled sources. | ||||
|      * Selected source. | ||||
|      */ | ||||
|     val sources by lazy { getEnabledSources() } | ||||
|  | ||||
|     /** | ||||
|      * Active source. | ||||
|      */ | ||||
|     lateinit var source: CatalogueSource | ||||
|         private set | ||||
|     val source = sourceManager.get(sourceId) as CatalogueSource | ||||
|  | ||||
|     /** | ||||
|      * Query from the view. | ||||
| @@ -106,7 +97,6 @@ open class CataloguePresenter( | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         source = getLastUsedSource() | ||||
|         sourceFilters = source.getFilterList() | ||||
|  | ||||
|         if (savedState != null) { | ||||
| @@ -141,17 +131,19 @@ open class CataloguePresenter( | ||||
|  | ||||
|         val sourceId = source.id | ||||
|  | ||||
|         val catalogueAsList = prefs.catalogueAsList() | ||||
|  | ||||
|         // Prepare the pager. | ||||
|         pagerSubscription?.let { remove(it) } | ||||
|         pagerSubscription = pager.results() | ||||
|                 .observeOn(Schedulers.io()) | ||||
|                 .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } } | ||||
|                 .doOnNext { initializeMangas(it.second) } | ||||
|                 .map { it.first to it.second.map(::CatalogueItem) } | ||||
|                 .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } } | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribeReplay({ view, pair -> | ||||
|                     view.onAddPage(pair.first, pair.second) | ||||
|                 }, { view, error -> | ||||
|                 .subscribeReplay({ view, (page, mangas) -> | ||||
|                     view.onAddPage(page, mangas) | ||||
|                 }, { _, error -> | ||||
|                     Timber.e(error) | ||||
|                 }) | ||||
|  | ||||
| @@ -167,7 +159,7 @@ open class CataloguePresenter( | ||||
|  | ||||
|         pageSubscription?.let { remove(it) } | ||||
|         pageSubscription = Observable.defer { pager.requestNext() } | ||||
|                 .subscribeFirst({ view, page -> | ||||
|                 .subscribeFirst({ _, _ -> | ||||
|                     // Nothing to do when onNext is emitted. | ||||
|                 }, CatalogueController::onAddPageError) | ||||
|     } | ||||
| @@ -179,19 +171,6 @@ open class CataloguePresenter( | ||||
|         return pager.hasNextPage | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the active source and restarts the pager. | ||||
|      * | ||||
|      * @param source the new active source. | ||||
|      */ | ||||
|     fun setActiveSource(source: CatalogueSource) { | ||||
|         prefs.lastUsedCatalogueSource().set(source.id) | ||||
|         this.source = source | ||||
|         sourceFilters = source.getFilterList() | ||||
|  | ||||
|         restartPager(query = "", filters = FilterList()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the display mode. | ||||
|      * | ||||
| @@ -267,50 +246,6 @@ open class CataloguePresenter( | ||||
|                 .onErrorResumeNext { Observable.just(manga) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the last used source from preferences or the first valid source. | ||||
|      * | ||||
|      * @return a source. | ||||
|      */ | ||||
|     fun getLastUsedSource(): CatalogueSource { | ||||
|         val id = prefs.lastUsedCatalogueSource().get() ?: -1 | ||||
|         val source = sourceManager.get(id) | ||||
|         if (!isValidSource(source) || source !in sources) { | ||||
|             return sources.first { isValidSource(it) } | ||||
|         } | ||||
|         return source as CatalogueSource | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if the given source is valid. | ||||
|      * | ||||
|      * @param source the source to check. | ||||
|      * @return true if the source is valid, false otherwise. | ||||
|      */ | ||||
|     open fun isValidSource(source: Source?): Boolean { | ||||
|         if (source == null) return false | ||||
|  | ||||
|         if (source is LoginSource) { | ||||
|             return source.isLogged() || | ||||
|                     (prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "") | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a list of enabled sources ordered by language and name. | ||||
|      */ | ||||
|     open protected fun getEnabledSources(): List<CatalogueSource> { | ||||
|         val languages = prefs.enabledLanguages().getOrDefault() | ||||
|         val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() | ||||
|  | ||||
|         return sourceManager.getCatalogueSources() | ||||
|                 .filter { it.lang in languages } | ||||
|                 .filterNot { it.id.toString() in hiddenCatalogues } | ||||
|                 .sortedBy { "(${it.lang}) ${it.name}" } + | ||||
|                 sourceManager.get(LocalSource.ID) as LocalSource | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds or removes a manga from the library. | ||||
|      * | ||||
| @@ -370,13 +305,12 @@ open class CataloguePresenter( | ||||
|                 } | ||||
|                 is Filter.Sort -> { | ||||
|                     val group = SortGroup(it) | ||||
|                     val subItems = it.values.mapNotNull { | ||||
|                     val subItems = it.values.map { | ||||
|                         SortItem(it, group) | ||||
|                     } | ||||
|                     group.subItems = subItems | ||||
|                     group | ||||
|                 } | ||||
|                 else -> null | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -407,7 +341,7 @@ open class CataloguePresenter( | ||||
|      * @param categories the selected categories. | ||||
|      * @param manga the manga to move. | ||||
|      */ | ||||
|     fun moveMangaToCategories(manga: Manga, categories: List<Category>) { | ||||
|     private fun moveMangaToCategories(manga: Manga, categories: List<Category>) { | ||||
|         val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } | ||||
|         db.setMangaCategories(mc, listOf(manga)) | ||||
|     } | ||||
|   | ||||
| @@ -1,30 +1,27 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.TextView | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() { | ||||
|  | ||||
|     var loadMore = true | ||||
|     private var loadMore = true | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_progress_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: Holder, position: Int, payloads: List<Any?>) { | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>) { | ||||
|         holder.progressBar.visibility = View.GONE | ||||
|         holder.progressMessage.visibility = View.GONE | ||||
|  | ||||
| @@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() { | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar | ||||
|         val progressMessage = view.findViewById(R.id.progress_message) as TextView | ||||
|         val progressBar: ProgressBar = view.findViewById(R.id.progress_bar) | ||||
|         val progressMessage: TextView = view.findViewById(R.id.progress_message) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,8 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.CheckBox | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| @@ -16,8 +14,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec | ||||
|         return R.layout.navigation_view_checkbox | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -32,10 +30,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is CheckboxItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as CheckboxItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -44,6 +40,6 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val check = itemView.findViewById(R.id.nav_view_item) as CheckBox | ||||
|         val check: CheckBox = itemView.findViewById(R.id.nav_view_item) | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.ImageView | ||||
| import android.widget.TextView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| @@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|         return R.layout.navigation_view_group | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun getItemViewType(): Int { | ||||
|         return 101 | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is GroupItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as GroupItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -46,8 +46,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|  | ||||
|     open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) { | ||||
|  | ||||
|         val title = itemView.findViewById(R.id.title) as TextView | ||||
|         val icon = itemView.findViewById(R.id.expand_icon) as ImageView | ||||
|         val title: TextView = itemView.findViewById(R.id.title) | ||||
|         val icon: ImageView = itemView.findViewById(R.id.expand_icon) | ||||
|  | ||||
|         override fun shouldNotifyParentOnClick(): Boolean { | ||||
|             return true | ||||
|   | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.support.design.R | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.TextView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractHeaderItem | ||||
| @@ -18,8 +16,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold | ||||
|         return R.layout.design_navigation_item_subheader | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -29,10 +27,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is HeaderItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as HeaderItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|   | ||||
| @@ -15,10 +15,8 @@ class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISect | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is TriStateSectionItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as TriStateSectionItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -38,10 +36,8 @@ class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<Text | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is TextSectionItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as TextSectionItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -61,10 +57,8 @@ class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISect | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is CheckboxSectionItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as CheckboxSectionItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -84,10 +78,8 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is SelectSectionItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as SelectSectionItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.ArrayAdapter | ||||
| import android.widget.Spinner | ||||
| import android.widget.TextView | ||||
| @@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec | ||||
|         return R.layout.navigation_view_spinner | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -32,18 +30,16 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec | ||||
|                 android.R.layout.simple_spinner_item, filter.values).apply { | ||||
|             setDropDownViewResource(R.layout.common_spinner_item) | ||||
|         } | ||||
|         spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> | ||||
|             filter.state = position | ||||
|         spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos -> | ||||
|             filter.state = pos | ||||
|         } | ||||
|         spinner.setSelection(filter.state) | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is SelectItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as SelectItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -52,7 +48,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val text = itemView.findViewById(R.id.nav_view_item_text) as TextView | ||||
|         val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner | ||||
|         val text: TextView = itemView.findViewById(R.id.nav_view_item_text) | ||||
|         val spinner: Spinner = itemView.findViewById(R.id.nav_view_item) | ||||
|     } | ||||
| } | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.support.design.R | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractHeaderItem | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| @@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator | ||||
|         return R.layout.design_navigation_item_separator | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -27,10 +25,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is SeparatorItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as SeparatorItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem | ||||
| import eu.davidea.flexibleadapter.items.ISectionable | ||||
| @@ -12,13 +10,16 @@ import eu.kanade.tachiyomi.util.setVectorCompat | ||||
|  | ||||
| class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() { | ||||
|  | ||||
|     // Use an id instead of the layout res to allow to reuse the layout. | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.id.catalogue_filter_sort_group | ||||
|         return R.layout.navigation_view_group | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter) | ||||
|     override fun getItemViewType(): Int { | ||||
|         return 100 | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is SortGroup) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as SortGroup).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|   | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.support.graphics.drawable.VectorDrawableCompat | ||||
| import android.support.v4.content.ContextCompat | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.CheckedTextView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractSectionableItem | ||||
| @@ -15,13 +13,16 @@ import eu.kanade.tachiyomi.util.getResourceColor | ||||
|  | ||||
| class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) { | ||||
|  | ||||
|     // Use an id instead of the layout res to allow to reuse the layout. | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.id.catalogue_filter_sort_item | ||||
|         return R.layout.navigation_view_checkedtext | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter) | ||||
|     override fun getItemViewType(): Int { | ||||
|         return 102 | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -54,10 +55,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is SortItem) { | ||||
|             return name == other.name && group == other.group | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         other as SortItem | ||||
|         return name == other.name && group == other.group | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView | ||||
|         val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,9 +1,7 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.support.design.widget.TextInputLayout | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.EditText | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| @@ -18,8 +16,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol | ||||
|         return R.layout.navigation_view_text | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -34,10 +32,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is TextItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as TextItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -46,7 +42,7 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val wrapper  = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout | ||||
|         val edit = itemView.findViewById(R.id.nav_view_item) as EditText | ||||
|         val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper) | ||||
|         val edit: EditText = itemView.findViewById(R.id.nav_view_item) | ||||
|     } | ||||
| } | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter | ||||
|  | ||||
| import android.support.design.R | ||||
| import android.support.graphics.drawable.VectorDrawableCompat | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.CheckedTextView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| @@ -20,8 +18,12 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS | ||||
|         return TR.layout.navigation_view_checkedtext | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup?): Holder { | ||||
|         return Holder(inflater.inflate(layoutRes, parent, false), adapter) | ||||
|     override fun getItemViewType(): Int { | ||||
|         return 103 | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { | ||||
|         return Holder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { | ||||
| @@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other is TriStateItem) { | ||||
|             return filter == other.filter | ||||
|         } | ||||
|         return false | ||||
|         if (javaClass != other?.javaClass) return false | ||||
|         return filter == (other as TriStateItem).filter | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
| @@ -63,7 +63,7 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView | ||||
|         val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item) | ||||
|  | ||||
|         init { | ||||
|             // Align with native checkbox | ||||
|   | ||||
| @@ -0,0 +1,74 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.os.Parcelable | ||||
| import android.support.v7.widget.RecyclerView | ||||
| import android.util.SparseArray | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
|  | ||||
| /** | ||||
|  * Adapter that holds the search cards. | ||||
|  * | ||||
|  * @param controller instance of [CatalogueSearchController]. | ||||
|  */ | ||||
| class CatalogueSearchAdapter(val controller: CatalogueSearchController) : | ||||
|         FlexibleAdapter<CatalogueSearchItem>(null, controller, true) { | ||||
|  | ||||
|     /** | ||||
|      * Bundle where the view state of the holders is saved. | ||||
|      */ | ||||
|     private var bundle = Bundle() | ||||
|  | ||||
|     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>?) { | ||||
|         super.onBindViewHolder(holder, position, payloads) | ||||
|         restoreHolderState(holder) | ||||
|     } | ||||
|  | ||||
|     override fun onViewRecycled(holder: RecyclerView.ViewHolder) { | ||||
|         super.onViewRecycled(holder) | ||||
|         saveHolderState(holder, bundle) | ||||
|     } | ||||
|  | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         val holdersBundle = Bundle() | ||||
|         allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) } | ||||
|         outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle) | ||||
|         super.onSaveInstanceState(outState) | ||||
|     } | ||||
|  | ||||
|     override fun onRestoreInstanceState(savedInstanceState: Bundle) { | ||||
|         super.onRestoreInstanceState(savedInstanceState) | ||||
|         bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Saves the view state of the given holder. | ||||
|      * | ||||
|      * @param holder The holder to save. | ||||
|      * @param outState The bundle where the state is saved. | ||||
|      */ | ||||
|     private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) { | ||||
|         val key = "holder_${holder.adapterPosition}" | ||||
|         val holderState = SparseArray<Parcelable>() | ||||
|         holder.itemView.saveHierarchyState(holderState) | ||||
|         outState.putSparseParcelableArray(key, holderState) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restores the view state of the given holder. | ||||
|      * | ||||
|      * @param holder The holder to restore. | ||||
|      */ | ||||
|     private fun restoreHolderState(holder: RecyclerView.ViewHolder) { | ||||
|         val key = "holder_${holder.adapterPosition}" | ||||
|         val holderState = bundle.getSparseParcelableArray<Parcelable>(key) | ||||
|         if (holderState != null) { | ||||
|             holder.itemView.restoreHierarchyState(holderState) | ||||
|             bundle.remove(key) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private companion object { | ||||
|         const val HOLDER_BUNDLE_KEY = "holder_bundle" | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| /** | ||||
|  * Adapter that holds the manga items from search results. | ||||
|  * | ||||
|  * @param controller instance of [CatalogueSearchController]. | ||||
|  */ | ||||
| class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : | ||||
|         FlexibleAdapter<CatalogueSearchCardItem>(null, controller, true) { | ||||
|  | ||||
|     /** | ||||
|      * Listen for browse item clicks. | ||||
|      */ | ||||
|     val mangaClickListener: OnMangaClickListener = controller | ||||
|  | ||||
|     /** | ||||
|      * Listener which should be called when user clicks browse. | ||||
|      * Note: Should only be handled by [CatalogueSearchController] | ||||
|      */ | ||||
|     interface OnMangaClickListener { | ||||
|         fun onMangaClick(manga: Manga) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.view.View | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.widget.StateImageViewTarget | ||||
| import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* | ||||
|  | ||||
| class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) | ||||
|     : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|     init { | ||||
|         // Call onMangaClickListener when item is pressed. | ||||
|         itemView.setOnClickListener { | ||||
|             val item = adapter.getItem(adapterPosition) | ||||
|             if (item != null) { | ||||
|                 adapter.mangaClickListener.onMangaClick(item.manga) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun bind(manga: Manga) { | ||||
|         itemView.tvTitle.text = manga.title | ||||
|  | ||||
|         setImage(manga) | ||||
|     } | ||||
|  | ||||
|     fun setImage(manga: Manga) { | ||||
|         GlideApp.with(itemView.context).clear(itemView.itemImage) | ||||
|         if (!manga.thumbnail_url.isNullOrEmpty()) { | ||||
|             GlideApp.with(itemView.context) | ||||
|                     .load(manga) | ||||
|                     .diskCacheStrategy(DiskCacheStrategy.DATA) | ||||
|                     .centerCrop() | ||||
|                     .skipMemoryCache(true) | ||||
|                     .placeholder(android.R.color.transparent) | ||||
|                     .into(StateImageViewTarget(itemView.itemImage, itemView.progress)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() { | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_global_search_controller_card_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder { | ||||
|         return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder, | ||||
|                                 position: Int, payloads: List<Any?>?) { | ||||
|         holder.bind(manga) | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (other is CatalogueSearchCardItem) { | ||||
|             return manga.id == other.manga.id | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         return manga.id?.toInt() ?: 0 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,184 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| import android.support.v7.widget.SearchView | ||||
| import android.view.* | ||||
| import com.bluelinelabs.conductor.RouterTransaction | ||||
| import com.bluelinelabs.conductor.changehandler.FadeChangeHandler | ||||
| import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.* | ||||
|  | ||||
| /** | ||||
|  * This controller shows and manages the different search result in global search. | ||||
|  * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter] | ||||
|  * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search | ||||
|  */ | ||||
| class CatalogueSearchController(private val initialQuery: String? = null) : | ||||
|         NucleusController<CatalogueSearchPresenter>(), | ||||
|         CatalogueSearchCardAdapter.OnMangaClickListener { | ||||
|  | ||||
|     /** | ||||
|      * Adapter containing search results grouped by lang. | ||||
|      */ | ||||
|     private var adapter: CatalogueSearchAdapter? = null | ||||
|  | ||||
|     /** | ||||
|      * Called when controller is initialized. | ||||
|      */ | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initiate the view with [R.layout.catalogue_global_search_controller]. | ||||
|      * | ||||
|      * @param inflater used to load the layout xml. | ||||
|      * @param container containing parent views. | ||||
|      * @return inflated view | ||||
|      */ | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View { | ||||
|         return inflater.inflate(R.layout.catalogue_global_search_controller, container, false) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the title of controller. | ||||
|      * | ||||
|      * @return title. | ||||
|      */ | ||||
|     override fun getTitle(): String? { | ||||
|         return presenter.query | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create the [CatalogueSearchPresenter] used in controller. | ||||
|      * | ||||
|      * @return instance of [CatalogueSearchPresenter] | ||||
|      */ | ||||
|     override fun createPresenter(): CatalogueSearchPresenter { | ||||
|         return CatalogueSearchPresenter(initialQuery) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when manga in global search is clicked, opens manga. | ||||
|      * | ||||
|      * @param manga clicked item containing manga information. | ||||
|      */ | ||||
|     override fun onMangaClick(manga: Manga) { | ||||
|         // Open MangaController. | ||||
|         router.pushController(RouterTransaction.with(MangaController(manga, true)) | ||||
|                 .pushChangeHandler(FadeChangeHandler()) | ||||
|                 .popChangeHandler(FadeChangeHandler())) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds items to the options menu. | ||||
|      * | ||||
|      * @param menu menu containing options. | ||||
|      * @param inflater used to load the menu xml. | ||||
|      */ | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         // Inflate menu. | ||||
|         inflater.inflate(R.menu.catalogue_new_list, menu) | ||||
|  | ||||
|         // Initialize search menu | ||||
|         val searchItem = menu.findItem(R.id.action_search) | ||||
|         val searchView = searchItem.actionView as SearchView | ||||
|  | ||||
|         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 | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         searchView.queryTextChangeEvents() | ||||
|                 .filter { it.isSubmitted } | ||||
|                 .subscribeUntilDestroy { | ||||
|                     presenter.search(it.queryText().toString()) | ||||
|                     searchItem.collapseActionView() | ||||
|                     setTitle() // Update toolbar title | ||||
|                 } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the view is created | ||||
|      * | ||||
|      * @param view view of controller | ||||
|      * @param savedViewState information from previous state. | ||||
|      */ | ||||
|     override fun onViewCreated(view: View, savedViewState: Bundle?) { | ||||
|         super.onViewCreated(view, savedViewState) | ||||
|  | ||||
|         adapter = CatalogueSearchAdapter(this) | ||||
|  | ||||
|         with(view) { | ||||
|             // Create recycler and set adapter. | ||||
|             recycler.layoutManager = LinearLayoutManager(context) | ||||
|             recycler.adapter = adapter | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onSaveViewState(view: View, outState: Bundle) { | ||||
|         super.onSaveViewState(view, outState) | ||||
|         adapter?.onSaveInstanceState(outState) | ||||
|     } | ||||
|  | ||||
|     override fun onRestoreViewState(view: View, savedViewState: Bundle) { | ||||
|         super.onRestoreViewState(view, savedViewState) | ||||
|         adapter?.onRestoreInstanceState(savedViewState) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the view holder for the given manga. | ||||
|      * | ||||
|      * @param source used to find holder containing source | ||||
|      * @return the holder of the manga or null if it's not bound. | ||||
|      */ | ||||
|     private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? { | ||||
|         val adapter = adapter ?: return null | ||||
|  | ||||
|         adapter.allBoundViewHolders.forEach { holder -> | ||||
|             val item = adapter.getItem(holder.adapterPosition) | ||||
|             if (item != null && source.id == item.source.id) { | ||||
|                 return holder as CatalogueSearchHolder | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add search result to adapter. | ||||
|      * | ||||
|      * @param searchResult result of search. | ||||
|      */ | ||||
|     fun setItems(searchResult: List<CatalogueSearchItem>) { | ||||
|         adapter?.updateDataSet(searchResult) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called from the presenter when a manga is initialized. | ||||
|      * | ||||
|      * @param manga the initialized manga. | ||||
|      */ | ||||
|     fun onMangaInitialized(source: CatalogueSource, manga: Manga) { | ||||
|         getHolder(source)?.setImage(manga) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,100 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| import android.view.View | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.util.getResourceColor | ||||
| import eu.kanade.tachiyomi.util.gone | ||||
| import eu.kanade.tachiyomi.util.setVectorCompat | ||||
| import eu.kanade.tachiyomi.util.visible | ||||
| import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.* | ||||
|  | ||||
| /** | ||||
|  * Holder that binds the [CatalogueSearchItem] containing catalogue cards. | ||||
|  * | ||||
|  * @param view view of [CatalogueSearchItem] | ||||
|  * @param adapter instance of [CatalogueSearchAdapter] | ||||
|  */ | ||||
| class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|     /** | ||||
|      * Adapter containing manga from search results. | ||||
|      */ | ||||
|     private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller) | ||||
|  | ||||
|     private var lastBoundResults: List<CatalogueSearchCardItem>? = null | ||||
|  | ||||
|     init { | ||||
|         with(itemView) { | ||||
|             // Set layout horizontal. | ||||
|             recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) | ||||
|             recycler.adapter = mangaAdapter | ||||
|  | ||||
|             nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp, | ||||
|                     context.getResourceColor(android.R.attr.textColorHint)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Show the loading of source search result. | ||||
|      * | ||||
|      * @param item item of card. | ||||
|      */ | ||||
|     fun bind(item: CatalogueSearchItem) { | ||||
|         val source = item.source | ||||
|         val results = item.results | ||||
|  | ||||
|         with(itemView) { | ||||
|             // Set Title witch country code if available. | ||||
|             title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name | ||||
|  | ||||
|             when { | ||||
|                 results == null -> { | ||||
|                     progress.visible() | ||||
|                     nothing_found.gone() | ||||
|                 } | ||||
|                 results.isEmpty() -> { | ||||
|                     progress.gone() | ||||
|                     nothing_found.visible() | ||||
|                 } | ||||
|                 else -> { | ||||
|                     progress.gone() | ||||
|                     nothing_found.gone() | ||||
|                 } | ||||
|             } | ||||
|             if (results !== lastBoundResults) { | ||||
|                 mangaAdapter.updateDataSet(results) | ||||
|                 lastBoundResults = results | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called from the presenter when a manga is initialized. | ||||
|      * | ||||
|      * @param manga the initialized manga. | ||||
|      */ | ||||
|     fun setImage(manga: Manga) { | ||||
|         getHolder(manga)?.setImage(manga) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the view holder for the given manga. | ||||
|      * | ||||
|      * @param manga the manga to find. | ||||
|      * @return the holder of the manga or null if it's not bound. | ||||
|      */ | ||||
|     private fun getHolder(manga: Manga): CatalogueSearchCardHolder? { | ||||
|         mangaAdapter.allBoundViewHolders.forEach { holder -> | ||||
|             val item = mangaAdapter.getItem(holder.adapterPosition) | ||||
|             if (item != null && item.manga.id!! == manga.id!!) { | ||||
|                 return holder as CatalogueSearchCardHolder | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return null | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,64 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
|  | ||||
| /** | ||||
|  * Item that contains search result information. | ||||
|  * | ||||
|  * @param source contains information about search result. | ||||
|  */ | ||||
| class CatalogueSearchItem(val source: CatalogueSource, val results: List<CatalogueSearchCardItem>?) | ||||
|     : AbstractFlexibleItem<CatalogueSearchHolder>() { | ||||
|  | ||||
|     /** | ||||
|      * Set view. | ||||
|      * | ||||
|      * @return id of view | ||||
|      */ | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_global_search_controller_card | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create view holder (see [CatalogueSearchAdapter]. | ||||
|      * | ||||
|      * @return holder of view. | ||||
|      */ | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchHolder { | ||||
|         return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Bind item to view. | ||||
|      */ | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder, | ||||
|                                 position: Int, payloads: List<Any?>?) { | ||||
|         holder.bind(this) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Used to check if two items are equal. | ||||
|      * | ||||
|      * @return items are equal? | ||||
|      */ | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (other is CatalogueSearchItem) { | ||||
|             return source.id == other.source.id | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return hash code of item. | ||||
|      * | ||||
|      * @return hashcode | ||||
|      */ | ||||
|     override fun hashCode(): Int { | ||||
|         return source.id.toInt() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,215 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.global_search | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.source.online.LoginSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import rx.subjects.PublishSubject | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * Presenter of [CatalogueSearchController] | ||||
|  * Function calls should be done from here. UI calls should be done from the controller. | ||||
|  * | ||||
|  * @param sourceManager manages the different sources. | ||||
|  * @param db manages the database calls. | ||||
|  * @param preferencesHelper manages the preference calls. | ||||
|  */ | ||||
| class CatalogueSearchPresenter( | ||||
|         val initialQuery: String? = "", | ||||
|         val sourceManager: SourceManager = Injekt.get(), | ||||
|         val db: DatabaseHelper = Injekt.get(), | ||||
|         val preferencesHelper: PreferencesHelper = Injekt.get() | ||||
| ) : BasePresenter<CatalogueSearchController>() { | ||||
|  | ||||
|     /** | ||||
|      * Enabled sources. | ||||
|      */ | ||||
|     val sources by lazy { getEnabledSources() } | ||||
|  | ||||
|     /** | ||||
|      * Query from the view. | ||||
|      */ | ||||
|     var query = "" | ||||
|         private set | ||||
|  | ||||
|     /** | ||||
|      * Fetches the different sources by user settings. | ||||
|      */ | ||||
|     private var fetchSourcesSubscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Subject which fetches image of given manga. | ||||
|      */ | ||||
|     private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>() | ||||
|  | ||||
|     /** | ||||
|      * Subscription for fetching images of manga. | ||||
|      */ | ||||
|     private var fetchImageSubscription: Subscription? = null | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         // Perform a search with previous or initial state | ||||
|         search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty()) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroy() { | ||||
|         fetchSourcesSubscription?.unsubscribe() | ||||
|         fetchImageSubscription?.unsubscribe() | ||||
|         super.onDestroy() | ||||
|     } | ||||
|  | ||||
|     override fun onSave(state: Bundle) { | ||||
|         state.putString(CataloguePresenter::query.name, query) | ||||
|         super.onSave(state) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a list of enabled sources ordered by language and name. | ||||
|      * | ||||
|      * @return list containing enabled sources. | ||||
|      */ | ||||
|     private fun getEnabledSources(): List<CatalogueSource> { | ||||
|         val languages = preferencesHelper.enabledLanguages().getOrDefault() | ||||
|         val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() | ||||
|  | ||||
|         return sourceManager.getCatalogueSources() | ||||
|                 .filter { it.lang in languages } | ||||
|                 .filterNot { it is LoginSource && !it.isLogged() } | ||||
|                 .filterNot { it.id.toString() in hiddenCatalogues } | ||||
|                 .sortedBy { "(${it.lang}) ${it.name}" } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initiates a search for mnaga per catalogue. | ||||
|      * | ||||
|      * @param query query on which to search. | ||||
|      */ | ||||
|     fun search(query: String) { | ||||
|         // Return if there's nothing to do | ||||
|         if (this.query == query) return | ||||
|  | ||||
|         // Update query | ||||
|         this.query = query | ||||
|  | ||||
|         // Create image fetch subscription | ||||
|         initializeFetchImageSubscription() | ||||
|  | ||||
|         // Create items with the initial state | ||||
|         val initialItems = sources.map { CatalogueSearchItem(it, null) } | ||||
|         var items = initialItems | ||||
|  | ||||
|         fetchSourcesSubscription?.unsubscribe() | ||||
|         fetchSourcesSubscription = Observable.from(sources) | ||||
|                 .flatMap({ source -> | ||||
|                     source.fetchSearchManga(1, query, FilterList()) | ||||
|                             .subscribeOn(Schedulers.io()) | ||||
|                             .onExceptionResumeNext(Observable.empty()) // Ignore timeouts. | ||||
|                             .map { it.mangas.take(10) } // Get at most 10 manga from search result. | ||||
|                             .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga. | ||||
|                             .doOnNext { fetchImage(it, source) } // Load manga covers. | ||||
|                             .map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) } | ||||
|                 }, 5) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 // Update matching source with the obtained results | ||||
|                 .map { result -> | ||||
|                     items.map { item -> if (item.source == result.source) result else item } | ||||
|                 } | ||||
|                 // Update current state | ||||
|                 .doOnNext { items = it } | ||||
|                 // Deliver initial state | ||||
|                 .startWith(initialItems) | ||||
|                 .subscribeLatestCache({ view, manga -> | ||||
|                     view.setItems(manga) | ||||
|                 }, { _, error -> | ||||
|                     Timber.e(error) | ||||
|                 }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initialize a list of manga. | ||||
|      * | ||||
|      * @param manga the list of manga to initialize. | ||||
|      */ | ||||
|     private fun fetchImage(manga: List<Manga>, source: Source) { | ||||
|         fetchImageSubject.onNext(Pair(manga, source)) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Subscribes to the initializer of manga details and updates the view if needed. | ||||
|      */ | ||||
|     private fun initializeFetchImageSubscription() { | ||||
|         fetchImageSubscription?.unsubscribe() | ||||
|         fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) | ||||
|                 .flatMap { | ||||
|                     val source = it.second | ||||
|                     Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized } | ||||
|                             .map { Pair(it, source) } | ||||
|                             .concatMap { getMangaDetailsObservable(it.first, it.second) } | ||||
|                             .map { Pair(source as CatalogueSource, it) } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|                 .onBackpressureBuffer() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ (source, manga) -> | ||||
|                     @Suppress("DEPRECATION") | ||||
|                     view?.onMangaInitialized(source, manga) | ||||
|                 }, { error -> | ||||
|                     Timber.e(error) | ||||
|                 }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable of manga that initializes the given manga. | ||||
|      * | ||||
|      * @param manga the manga to initialize. | ||||
|      * @return an observable of the manga to initialize | ||||
|      */ | ||||
|     private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable<Manga> { | ||||
|         return source.fetchMangaDetails(manga) | ||||
|                 .flatMap { networkManga -> | ||||
|                     manga.copyFrom(networkManga) | ||||
|                     manga.initialized = true | ||||
|                     db.insertManga(manga).executeAsBlocking() | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|                 .onErrorResumeNext { Observable.just(manga) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a manga from the database for the given manga from network. It creates a new entry | ||||
|      * if the manga is not yet in the database. | ||||
|      * | ||||
|      * @param sManga the manga from the source. | ||||
|      * @return a manga from the database. | ||||
|      */ | ||||
|     private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { | ||||
|         var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking() | ||||
|         if (localManga == null) { | ||||
|             val newManga = Manga.create(sManga.url, sManga.title, sourceId) | ||||
|             newManga.copyFrom(sManga) | ||||
|             val result = db.insertManga(newManga).executeAsBlocking() | ||||
|             newManga.id = result.insertedId() | ||||
|             localManga = newManga | ||||
|         } | ||||
|         return localManga | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.getResourceColor | ||||
|  | ||||
| /** | ||||
|  * Adapter that holds the catalogue cards. | ||||
|  * | ||||
|  * @param controller instance of [CatalogueMainController]. | ||||
|  */ | ||||
| class CatalogueMainAdapter(val controller: CatalogueMainController) : | ||||
|         FlexibleAdapter<IFlexible<*>>(null, controller, true) { | ||||
|  | ||||
|     val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) | ||||
|  | ||||
|     init { | ||||
|         setDisplayHeadersAtStartUp(true) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Listener for browse item clicks. | ||||
|      */ | ||||
|     val browseClickListener: OnBrowseClickListener = controller | ||||
|  | ||||
|     /** | ||||
|      * Listener for latest item clicks. | ||||
|      */ | ||||
|     val latestClickListener: OnLatestClickListener = controller | ||||
|  | ||||
|     /** | ||||
|      * Listener which should be called when user clicks browse. | ||||
|      * Note: Should only be handled by [CatalogueMainController] | ||||
|      */ | ||||
|     interface OnBrowseClickListener { | ||||
|         fun onBrowseClick(position: Int) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Listener which should be called when user clicks latest. | ||||
|      * Note: Should only be handled by [CatalogueMainController] | ||||
|      */ | ||||
|     interface OnLatestClickListener { | ||||
|         fun onLatestClick(position: Int) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,238 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| import android.support.v7.widget.SearchView | ||||
| import android.view.* | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.bluelinelabs.conductor.RouterTransaction | ||||
| import com.bluelinelabs.conductor.changehandler.FadeChangeHandler | ||||
| import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.online.LoginSource | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CatalogueController | ||||
| import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController | ||||
| import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController | ||||
| import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog | ||||
| import kotlinx.android.synthetic.main.catalogue_main_controller.view.* | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * This controller shows and manages the different catalogues enabled by the user. | ||||
|  * This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter] | ||||
|  * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues. | ||||
|  * [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click. | ||||
|  * [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click | ||||
|  */ | ||||
| class CatalogueMainController : NucleusController<CatalogueMainPresenter>(), | ||||
|         SourceLoginDialog.Listener, | ||||
|         FlexibleAdapter.OnItemClickListener, | ||||
|         CatalogueMainAdapter.OnBrowseClickListener, | ||||
|         CatalogueMainAdapter.OnLatestClickListener { | ||||
|  | ||||
|     /** | ||||
|      * Application preferences. | ||||
|      */ | ||||
|     private val preferences: PreferencesHelper = Injekt.get() | ||||
|  | ||||
|     /** | ||||
|      * Adapter containing sources. | ||||
|      */ | ||||
|     private var adapter : CatalogueMainAdapter? = null | ||||
|  | ||||
|     /** | ||||
|      * Called when controller is initialized. | ||||
|      */ | ||||
|     init { | ||||
|         // Enable the option menu | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the title of controller. | ||||
|      * | ||||
|      * @return title. | ||||
|      */ | ||||
|     override fun getTitle(): String? { | ||||
|         return applicationContext?.getString(R.string.label_catalogues) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create the [CatalogueMainPresenter] used in controller. | ||||
|      * | ||||
|      * @return instance of [CatalogueMainPresenter] | ||||
|      */ | ||||
|     override fun createPresenter(): CatalogueMainPresenter { | ||||
|         return CatalogueMainPresenter() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initiate the view with [R.layout.catalogue_main_controller]. | ||||
|      * | ||||
|      * @param inflater used to load the layout xml. | ||||
|      * @param container containing parent views. | ||||
|      * @return inflated view. | ||||
|      */ | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { | ||||
|         return inflater.inflate(R.layout.catalogue_main_controller, container, false) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the view is created | ||||
|      * | ||||
|      * @param view view of controller | ||||
|      * @param savedViewState information from previous state. | ||||
|      */ | ||||
|     override fun onViewCreated(view: View, savedViewState: Bundle?) { | ||||
|         super.onViewCreated(view, savedViewState) | ||||
|  | ||||
|         adapter = CatalogueMainAdapter(this) | ||||
|  | ||||
|         with(view) { | ||||
|             // Create recycler and set adapter. | ||||
|             recycler.layoutManager = LinearLayoutManager(context) | ||||
|             recycler.adapter = adapter | ||||
|             recycler.addItemDecoration(SourceDividerItemDecoration(context)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) { | ||||
|             presenter.updateSources() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when login dialog is closed, refreshes the adapter. | ||||
|      * | ||||
|      * @param source clicked item containing source information. | ||||
|      */ | ||||
|     override fun loginDialogClosed(source: LoginSource) { | ||||
|         if (source.isLogged()) { | ||||
|             adapter?.clear() | ||||
|             presenter.loadSources() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when item is clicked | ||||
|      */ | ||||
|     override fun onItemClick(position: Int): Boolean { | ||||
|         val item = adapter?.getItem(position) as? SourceItem ?: return false | ||||
|         val source = item.source | ||||
|         if (source is LoginSource && !source.isLogged()) { | ||||
|             val dialog = SourceLoginDialog(source) | ||||
|             dialog.targetController = this | ||||
|             dialog.showDialog(router) | ||||
|         } else { | ||||
|             // Open the catalogue view. | ||||
|             openCatalogue(source, CatalogueController(source)) | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when browse is clicked in [CatalogueMainAdapter] | ||||
|      */ | ||||
|     override fun onBrowseClick(position: Int) { | ||||
|         onItemClick(position) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when latest is clicked in [CatalogueMainAdapter] | ||||
|      */ | ||||
|     override fun onLatestClick(position: Int) { | ||||
|         val item = adapter?.getItem(position) as? SourceItem ?: return | ||||
|         openCatalogue(item.source, LatestUpdatesController(item.source)) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Opens a catalogue with the given controller. | ||||
|      */ | ||||
|     private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) { | ||||
|         preferences.lastUsedCatalogueSource().set(source.id) | ||||
|         router.pushController(RouterTransaction.with(controller) | ||||
|                 .popChangeHandler(FadeChangeHandler()) | ||||
|                 .pushChangeHandler(FadeChangeHandler())) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds items to the options menu. | ||||
|      * | ||||
|      * @param menu menu containing options. | ||||
|      * @param inflater used to load the menu xml. | ||||
|      */ | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         // Inflate menu | ||||
|         inflater.inflate(R.menu.catalogue_main, menu) | ||||
|  | ||||
|         // Initialize search option. | ||||
|         val searchItem = menu.findItem(R.id.action_search) | ||||
|         val searchView = searchItem.actionView as SearchView | ||||
|  | ||||
|         // Change hint to show global search. | ||||
|         searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) | ||||
|  | ||||
|         // Create query listener which opens the global search view. | ||||
|         searchView.queryTextChangeEvents() | ||||
|                 .filter { it.isSubmitted } | ||||
|                 .subscribeUntilDestroy { | ||||
|                     val query = it.queryText().toString() | ||||
|                     router.pushController((RouterTransaction.with(CatalogueSearchController(query))) | ||||
|                             .popChangeHandler(FadeChangeHandler()) | ||||
|                             .pushChangeHandler(FadeChangeHandler())) | ||||
|                 } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when an option menu item has been selected by the user. | ||||
|      * | ||||
|      * @param item The selected item. | ||||
|      * @return True if this event has been consumed, false if it has not. | ||||
|      */ | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             // Initialize option to open catalogue settings. | ||||
|             R.id.action_settings -> { | ||||
|                 router.pushController((RouterTransaction.with(SettingsSourcesController())) | ||||
|                         .popChangeHandler(SettingsSourcesFadeChangeHandler()) | ||||
|                         .pushChangeHandler(FadeChangeHandler())) | ||||
|             } | ||||
|             else -> return super.onOptionsItemSelected(item) | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called to update adapter containing sources. | ||||
|      */ | ||||
|     fun setSources(sources: List<IFlexible<*>>) { | ||||
|         adapter?.updateDataSet(sources) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called to set the last used catalogue at the top of the view. | ||||
|      */ | ||||
|     fun setLastUsedSource(item: SourceItem?) { | ||||
|         adapter?.removeAllScrollableHeaders() | ||||
|         if (item != null) { | ||||
|             adapter?.addScrollableHeader(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class SettingsSourcesFadeChangeHandler : FadeChangeHandler() | ||||
| } | ||||
| @@ -0,0 +1,104 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| 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 rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.* | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * Presenter of [CatalogueMainController] | ||||
|  * Function calls should be done from here. UI calls should be done from the controller. | ||||
|  * | ||||
|  * @param sourceManager manages the different sources. | ||||
|  * @param preferences application preferences. | ||||
|  */ | ||||
| class CatalogueMainPresenter( | ||||
|         val sourceManager: SourceManager = Injekt.get(), | ||||
|         private val preferences: PreferencesHelper = Injekt.get() | ||||
| ) : BasePresenter<CatalogueMainController>() { | ||||
|  | ||||
|     /** | ||||
|      * Enabled sources. | ||||
|      */ | ||||
|     var sources = getEnabledSources() | ||||
|  | ||||
|     /** | ||||
|      * Subscription for retrieving enabled sources. | ||||
|      */ | ||||
|     private var sourceSubscription: Subscription? = null | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         // Load enabled and last used sources | ||||
|         loadSources() | ||||
|         loadLastUsedSource() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Unsubscribe and create a new subscription to fetch enabled sources. | ||||
|      */ | ||||
|     fun loadSources() { | ||||
|         sourceSubscription?.unsubscribe() | ||||
|  | ||||
|         val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 -> | ||||
|             // Catalogues without a lang defined will be placed at the end | ||||
|             when { | ||||
|                 d1 == "" && d2 != "" -> 1 | ||||
|                 d2 == "" && d1 != "" -> -1 | ||||
|                 else -> d1.compareTo(d2) | ||||
|             } | ||||
|         } | ||||
|         val byLang = sources.groupByTo(map, { it.lang }) | ||||
|         val sourceItems = byLang.flatMap { | ||||
|             val langItem = LangItem(it.key) | ||||
|             it.value.map { source -> SourceItem(source, langItem) } | ||||
|         } | ||||
|  | ||||
|         sourceSubscription = Observable.just(sourceItems) | ||||
|                 .subscribeLatestCache(CatalogueMainController::setSources) | ||||
|     } | ||||
|  | ||||
|     private fun loadLastUsedSource() { | ||||
|         val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share() | ||||
|  | ||||
|         // Emit the first item immediately but delay subsequent emissions by 500ms. | ||||
|         Observable.merge( | ||||
|                 sharedObs.take(1), | ||||
|                 sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())) | ||||
|                 .distinctUntilChanged() | ||||
|                 .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } } | ||||
|                 .subscribeLatestCache(CatalogueMainController::setLastUsedSource) | ||||
|     } | ||||
|  | ||||
|     fun updateSources() { | ||||
|         sources = getEnabledSources() | ||||
|         loadSources() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a list of enabled sources ordered by language and name. | ||||
|      * | ||||
|      * @return list containing enabled sources. | ||||
|      */ | ||||
|     private fun getEnabledSources(): List<CatalogueSource> { | ||||
|         val languages = preferences.enabledLanguages().getOrDefault() | ||||
|         val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault() | ||||
|  | ||||
|         return sourceManager.getCatalogueSources() | ||||
|                 .filter { it.lang in languages } | ||||
|                 .filterNot { it.id.toString() in hiddenCatalogues } | ||||
|                 .sortedBy { "(${it.lang}) ${it.name}" } + | ||||
|                 sourceManager.get(LocalSource.ID) as LocalSource | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.* | ||||
| import java.util.* | ||||
|  | ||||
| class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { | ||||
|  | ||||
|     fun bind(item: LangItem) { | ||||
|         itemView.title.text = when { | ||||
|             item.code == "" -> itemView.context.getString(R.string.other_source) | ||||
|             else -> { | ||||
|                 val locale = Locale(item.code) | ||||
|                 locale.getDisplayName(locale).capitalize() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractHeaderItem | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| /** | ||||
|  * Item that contains the language header. | ||||
|  * | ||||
|  * @param code The lang code. | ||||
|  */ | ||||
| data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() { | ||||
|  | ||||
|     /** | ||||
|      * Returns the layout resource of this item. | ||||
|      */ | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_main_controller_card | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new view holder for this item. | ||||
|      */ | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LangHolder { | ||||
|         return LangHolder(view, adapter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds this item to the given view holder. | ||||
|      */ | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder, | ||||
|                                 position: Int, payloads: List<Any?>?) { | ||||
|  | ||||
|         holder.bind(this) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Canvas | ||||
| import android.graphics.Rect | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.support.v7.widget.RecyclerView | ||||
| import android.view.View | ||||
|  | ||||
| class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { | ||||
|  | ||||
|     private val divider: Drawable | ||||
|  | ||||
|     init { | ||||
|         val a = context.obtainStyledAttributes(ATTRS) | ||||
|         divider = a.getDrawable(0) | ||||
|         a.recycle() | ||||
|     } | ||||
|  | ||||
|     override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { | ||||
|         val left = parent.paddingLeft + SourceHolder.margin | ||||
|         val right = parent.width - parent.paddingRight - SourceHolder.margin | ||||
|  | ||||
|         val childCount = parent.childCount | ||||
|         for (i in 0 until childCount - 1) { | ||||
|             val child = parent.getChildAt(i) | ||||
|             if (parent.getChildViewHolder(child) is SourceHolder && | ||||
|                     parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { | ||||
|                 val params = child.layoutParams as RecyclerView.LayoutParams | ||||
|                 val top = child.bottom + params.bottomMargin | ||||
|                 val bottom = top + divider.intrinsicHeight | ||||
|  | ||||
|                 divider.setBounds(left, top, right, bottom) | ||||
|                 divider.draw(c) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, | ||||
|                                 state: RecyclerView.State) { | ||||
|         outRect.set(0, 0, 0, divider.intrinsicHeight) | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private val ATTRS = intArrayOf(android.R.attr.listDivider) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,107 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.os.Build | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.online.LoginSource | ||||
| import eu.kanade.tachiyomi.util.dpToPx | ||||
| import eu.kanade.tachiyomi.util.getRound | ||||
| import eu.kanade.tachiyomi.util.gone | ||||
| import eu.kanade.tachiyomi.util.visible | ||||
| import io.github.mthli.slice.Slice | ||||
| import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.* | ||||
|  | ||||
| class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|     private val slice = Slice(itemView.card).apply { | ||||
|         setColor(adapter.cardBackground) | ||||
|     } | ||||
|  | ||||
|     init { | ||||
|         itemView.source_browse.setOnClickListener { | ||||
|             adapter.browseClickListener.onBrowseClick(adapterPosition) | ||||
|         } | ||||
|  | ||||
|         itemView.source_latest.setOnClickListener { | ||||
|             adapter.latestClickListener.onLatestClick(adapterPosition) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun bind(item: SourceItem) { | ||||
|         val source = item.source | ||||
|         with(itemView) { | ||||
|             setCardEdges(item) | ||||
|  | ||||
|             // Set source name | ||||
|             title.text = source.name | ||||
|  | ||||
|             // Set circle letter image. | ||||
|             post { | ||||
|                 image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) | ||||
|             } | ||||
|  | ||||
|             // If source is login, show only login option | ||||
|             if (source is LoginSource && !source.isLogged()) { | ||||
|                 source_browse.setText(R.string.login) | ||||
|                 source_latest.gone() | ||||
|             } else { | ||||
|                 source_browse.setText(R.string.browse) | ||||
|                 source_latest.visible() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun setCardEdges(item: SourceItem) { | ||||
|         // Position of this item in its header. Defaults to 0 when header is null. | ||||
|         var position = 0 | ||||
|  | ||||
|         // Number of items in the header of this item. Defaults to 1 when header is null. | ||||
|         var count = 1 | ||||
|  | ||||
|         if (item.header != null) { | ||||
|             val sectionItems = mAdapter.getSectionItems(item.header) | ||||
|             position = sectionItems.indexOf(item) | ||||
|             count = sectionItems.size | ||||
|         } | ||||
|  | ||||
|         when { | ||||
|             // Only one item in the card | ||||
|             count == 1 -> applySlice(2f, false, false, true, true) | ||||
|             // First item of the card | ||||
|             position == 0 -> applySlice(2f, false, true, true, false) | ||||
|             // Last item of the card | ||||
|             position == count - 1 -> applySlice(2f, true, false, false, true) | ||||
|             // Middle item | ||||
|             else -> applySlice(0f, false, false, false, false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, | ||||
|                            topShadow: Boolean, bottomShadow: Boolean) { | ||||
|  | ||||
|         slice.setRadius(radius) | ||||
|         slice.showLeftTopRect(topRect) | ||||
|         slice.showRightTopRect(topRect) | ||||
|         slice.showLeftBottomRect(bottomRect) | ||||
|         slice.showRightBottomRect(bottomRect) | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             slice.showTopEdgeShadow(topShadow) | ||||
|             slice.showBottomEdgeShadow(bottomShadow) | ||||
|         } | ||||
|         setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) | ||||
|     } | ||||
|  | ||||
|     private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { | ||||
|         val v = itemView.card | ||||
|         if (v.layoutParams is ViewGroup.MarginLayoutParams) { | ||||
|             val p = v.layoutParams as ViewGroup.MarginLayoutParams | ||||
|             p.setMargins(left, top, right, bottom) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         val margin = 8.dpToPx | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue.main | ||||
|  | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractSectionableItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
|  | ||||
| /** | ||||
|  * Item that contains source information. | ||||
|  * | ||||
|  * @param source Instance of [CatalogueSource] containing source information. | ||||
|  * @param header The header for this item. | ||||
|  */ | ||||
| data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) : | ||||
|         AbstractSectionableItem<SourceHolder, LangItem>(header) { | ||||
|  | ||||
|     /** | ||||
|      * Returns the layout resource of this item. | ||||
|      */ | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_main_controller_card_item | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new view holder for this item. | ||||
|      */ | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { | ||||
|         return SourceHolder(view, adapter as CatalogueMainAdapter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds this item to the given view holder. | ||||
|      */ | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder, | ||||
|                                 position: Int, payloads: List<Any?>?) { | ||||
|  | ||||
|         holder.bind(this) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) : | ||||
|      */ | ||||
|     override fun clearSelection() { | ||||
|         super.clearSelection() | ||||
|         (0 until itemCount).forEach { getItem(it).isSelected = false } | ||||
|         (0 until itemCount).forEach { getItem(it)?.isSelected = false } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clears the active selections from the model. | ||||
|      */ | ||||
|     fun clearModelSelection() { | ||||
|         selectedPositions.forEach { getItem(it).isSelected = false } | ||||
|         selectedPositions.forEach { getItem(it)?.isSelected = false } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -37,7 +37,7 @@ class CategoryAdapter(controller: CategoryController) : | ||||
|      */ | ||||
|     override fun toggleSelection(position: Int) { | ||||
|         super.toggleSelection(position) | ||||
|         getItem(position).isSelected = isSelected(position) | ||||
|         getItem(position)?.isSelected = isSelected(position) | ||||
|     } | ||||
|  | ||||
|     interface OnItemReleaseListener { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package eu.kanade.tachiyomi.ui.category | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.support.design.widget.Snackbar | ||||
| import android.support.v7.app.AppCompatActivity | ||||
| import android.support.v7.view.ActionMode | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| @@ -8,11 +9,12 @@ import android.support.v7.widget.RecyclerView | ||||
| import android.view.* | ||||
| import com.jakewharton.rxbinding.view.clicks | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.SelectableAdapter | ||||
| import eu.davidea.flexibleadapter.helpers.UndoHelper | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.util.toast | ||||
| import eu.kanade.tachiyomi.widget.UndoHelper | ||||
| import kotlinx.android.synthetic.main.categories_controller.view.* | ||||
|  | ||||
| /** | ||||
| @@ -38,7 +40,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|     private var adapter: CategoryAdapter? = null | ||||
|  | ||||
|     /** | ||||
|      * Undo helper for deleting categories. | ||||
|      * Undo helper used for restoring a deleted category. | ||||
|      */ | ||||
|     private var undoHelper: UndoHelper? = null | ||||
|  | ||||
| @@ -79,6 +81,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|             recycler.setHasFixedSize(true) | ||||
|             recycler.adapter = adapter | ||||
|             adapter?.isHandleDragEnabled = true | ||||
|             adapter?.isPermanentDelete = false | ||||
|  | ||||
|             fab.clicks().subscribeUntilDestroy { | ||||
|                 CategoryCreateDialog(this@CategoryController).showDialog(router, null) | ||||
| @@ -93,7 +96,8 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|      */ | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         undoHelper?.dismissNow() // confirm categories deletion if required | ||||
|         // Manually call callback to delete categories if required | ||||
|         undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) | ||||
|         undoHelper = null | ||||
|         actionMode = null | ||||
|         adapter = null | ||||
| @@ -106,7 +110,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|      */ | ||||
|     fun setCategories(categories: List<CategoryItem>) { | ||||
|         actionMode?.finish() | ||||
|         adapter?.updateDataSet(categories.toMutableList()) | ||||
|         adapter?.updateDataSet(categories) | ||||
|         val selected = categories.filter { it.isSelected } | ||||
|         if (selected.isNotEmpty()) { | ||||
|             selected.forEach { onItemLongClick(categories.indexOf(it)) } | ||||
| @@ -126,7 +130,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|         // Inflate menu. | ||||
|         mode.menuInflater.inflate(R.menu.category_selection, menu) | ||||
|         // Enable adapter multi selection. | ||||
|         adapter?.mode = FlexibleAdapter.MODE_MULTI | ||||
|         adapter?.mode = SelectableAdapter.Mode.MULTI | ||||
|         return true | ||||
|     } | ||||
|  | ||||
| @@ -161,26 +165,20 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|  | ||||
|         when (item.itemId) { | ||||
|             R.id.action_delete -> { | ||||
|                 undoHelper = UndoHelper(adapter, this).apply { | ||||
|                     withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener { | ||||
|                         override fun onPreAction(): Boolean { | ||||
|                             adapter.clearModelSelection() | ||||
|                             return false | ||||
|                         } | ||||
|                 undoHelper = UndoHelper(adapter, this) | ||||
|                 undoHelper?.start(adapter.selectedPositions, view!!, | ||||
|                                 R.string.snack_categories_deleted, R.string.action_undo, 3000) | ||||
|  | ||||
|                         override fun onPostAction() { | ||||
|                             mode.finish() | ||||
|                         } | ||||
|                     }) | ||||
|                     remove(adapter.selectedPositions, view!!, | ||||
|                             R.string.snack_categories_deleted, R.string.action_undo, 3000) | ||||
|                 } | ||||
|                 mode.finish() | ||||
|             } | ||||
|             R.id.action_edit -> { | ||||
|                 // Edit selected category | ||||
|                 if (adapter.selectedItemCount == 1) { | ||||
|                     val position = adapter.selectedPositions.first() | ||||
|                     editCategory(adapter.getItem(position).category) | ||||
|                     val category = adapter.getItem(position)?.category | ||||
|                     if (category != null) { | ||||
|                         editCategory(category) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else -> return false | ||||
| @@ -195,7 +193,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|      */ | ||||
|     override fun onDestroyActionMode(mode: ActionMode) { | ||||
|         // Reset adapter to single selection | ||||
|         adapter?.mode = FlexibleAdapter.MODE_IDLE | ||||
|         adapter?.mode = SelectableAdapter.Mode.IDLE | ||||
|         adapter?.clearSelection() | ||||
|         actionMode = null | ||||
|     } | ||||
| @@ -260,7 +258,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|      */ | ||||
|     override fun onItemReleased(position: Int) { | ||||
|         val adapter = adapter ?: return | ||||
|         val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category } | ||||
|         val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category } | ||||
|         presenter.reorderCategories(categories) | ||||
|     } | ||||
|  | ||||
| @@ -269,18 +267,21 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|      * | ||||
|      * @param action The action performed. | ||||
|      */ | ||||
|     override fun onUndoConfirmed(action: Int) { | ||||
|     override fun onActionCanceled(action: Int) { | ||||
|         adapter?.restoreDeletedItems() | ||||
|         undoHelper = null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the time to restore the items expires. | ||||
|      * | ||||
|      * @param action The action performed. | ||||
|      * @param event The event that triggered the action | ||||
|      */ | ||||
|     override fun onDeleteConfirmed(action: Int) { | ||||
|     override fun onActionConfirmed(action: Int, event: Int) { | ||||
|         val adapter = adapter ?: return | ||||
|         presenter.deleteCategories(adapter.deletedItems.map { it.category }) | ||||
|         undoHelper = null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable | ||||
| import com.amulyakhare.textdrawable.util.ColorGenerator | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.util.getRound | ||||
| import kotlinx.android.synthetic.main.categories_item.view.* | ||||
|  | ||||
| /** | ||||
| @@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol | ||||
|  | ||||
|         // Update circle letter image. | ||||
|         itemView.post { | ||||
|             itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase())) | ||||
|             itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns circle letter image. | ||||
|      * | ||||
|      * @param text The first letter of string. | ||||
|      */ | ||||
|     private fun getRound(text: String): TextDrawable { | ||||
|         val size = Math.min(itemView.image.width, itemView.image.height) | ||||
|         return TextDrawable.builder() | ||||
|                 .beginConfig() | ||||
|                 .width(size) | ||||
|                 .height(size) | ||||
|                 .textColor(Color.WHITE) | ||||
|                 .useFont(Typeface.DEFAULT) | ||||
|                 .endConfig() | ||||
|                 .buildRound(text, ColorGenerator.MATERIAL.getColor(text)) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when an item is released. | ||||
|      * | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.ui.category | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.util.inflate | ||||
|  | ||||
| /** | ||||
|  * Category item for a recycler view. | ||||
| @@ -28,15 +26,11 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder | ||||
|     /** | ||||
|      * Returns a new view holder for this item. | ||||
|      * | ||||
|      * @param view The view of this item. | ||||
|      * @param adapter The adapter of this item. | ||||
|      * @param inflater The layout inflater for XML inflation. | ||||
|      * @param parent The container view. | ||||
|      */ | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, | ||||
|                                   inflater: LayoutInflater, | ||||
|                                   parent: ViewGroup): CategoryHolder { | ||||
|  | ||||
|         return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CategoryHolder { | ||||
|         return CategoryHolder(view, adapter as CategoryAdapter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() { | ||||
|         downloadQueue.getUpdatedObservable() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .map { ArrayList(it) } | ||||
|                 .subscribeLatestCache(DownloadController::onNextDownloads, { view, error -> | ||||
|                 .subscribeLatestCache(DownloadController::onNextDownloads, { _, error -> | ||||
|                     Timber.e(error) | ||||
|                 }) | ||||
|     } | ||||
|   | ||||
| @@ -1,19 +1,25 @@ | ||||
| package eu.kanade.tachiyomi.ui.latest_updates | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.support.v4.widget.DrawerLayout | ||||
| import android.view.Menu | ||||
| import android.view.ViewGroup | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CatalogueController | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter | ||||
|  | ||||
| /** | ||||
|  * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. | ||||
|  * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. | ||||
|  */ | ||||
| class LatestUpdatesController : CatalogueController() { | ||||
| class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) { | ||||
|  | ||||
|     constructor(source: CatalogueSource) : this(Bundle().apply { | ||||
|         putLong(SOURCE_ID_KEY, source.id) | ||||
|     }) | ||||
|  | ||||
|     override fun createPresenter(): CataloguePresenter { | ||||
|         return LatestUpdatesPresenter() | ||||
|         return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareOptionsMenu(menu: Menu) { | ||||
| @@ -30,4 +36,4 @@ class LatestUpdatesController : CatalogueController() { | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.ui.latest_updates | ||||
|  | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter | ||||
| import eu.kanade.tachiyomi.ui.catalogue.Pager | ||||
| @@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager | ||||
| /** | ||||
|  * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. | ||||
|  */ | ||||
| class LatestUpdatesPresenter : CataloguePresenter() { | ||||
| class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) { | ||||
|  | ||||
|     override fun createPager(query: String, filters: FilterList): Pager { | ||||
|         return LatestUpdatesPager(source) | ||||
|     } | ||||
|  | ||||
|     override fun getEnabledSources(): List<CatalogueSource> { | ||||
|         return super.getEnabledSources().filter { it.supportsLatest } | ||||
|     } | ||||
|  | ||||
|     override fun isValidSource(source: Source?): Boolean { | ||||
|         return super.isValidSource(source) && (source as CatalogueSource).supportsLatest | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView | ||||
| import android.util.AttributeSet | ||||
| import android.widget.FrameLayout | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.SelectableAdapter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| @@ -103,9 +104,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|         this.category = category | ||||
|  | ||||
|         adapter.mode = if (controller.selectedMangas.isNotEmpty()) { | ||||
|             FlexibleAdapter.MODE_MULTI | ||||
|             SelectableAdapter.Mode.MULTI | ||||
|         } else { | ||||
|             FlexibleAdapter.MODE_SINGLE | ||||
|             SelectableAdapter.Mode.SINGLE | ||||
|         } | ||||
|  | ||||
|         subscriptions += controller.searchRelay | ||||
| @@ -126,7 +127,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|         subscriptions.clear() | ||||
|     } | ||||
|  | ||||
|  | ||||
|     override fun onDetachedFromWindow() { | ||||
|         subscriptions.clear() | ||||
|         super.onDetachedFromWindow() | ||||
| @@ -145,7 +145,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|         // Update the category with its manga. | ||||
|         adapter.setItems(mangaForCategory) | ||||
|  | ||||
|         if (adapter.mode == FlexibleAdapter.MODE_MULTI) { | ||||
|         if (adapter.mode == SelectableAdapter.Mode.MULTI) { | ||||
|             controller.selectedMangas.forEach { manga -> | ||||
|                 val position = adapter.indexOf(manga) | ||||
|                 if (position != -1 && !adapter.isSelected(position)) { | ||||
| @@ -165,19 +165,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|     private fun onSelectionChanged(event: LibrarySelectionEvent) { | ||||
|         when (event) { | ||||
|             is LibrarySelectionEvent.Selected -> { | ||||
|                 if (adapter.mode != FlexibleAdapter.MODE_MULTI) { | ||||
|                     adapter.mode = FlexibleAdapter.MODE_MULTI | ||||
|                 if (adapter.mode != SelectableAdapter.Mode.MULTI) { | ||||
|                     adapter.mode = SelectableAdapter.Mode.MULTI | ||||
|                 } | ||||
|                 findAndToggleSelection(event.manga) | ||||
|             } | ||||
|             is LibrarySelectionEvent.Unselected -> { | ||||
|                 findAndToggleSelection(event.manga) | ||||
|                 if (controller.selectedMangas.isEmpty()) { | ||||
|                     adapter.mode = FlexibleAdapter.MODE_SINGLE | ||||
|                     adapter.mode = SelectableAdapter.Mode.SINGLE | ||||
|                 } | ||||
|             } | ||||
|             is LibrarySelectionEvent.Cleared -> { | ||||
|                 adapter.mode = FlexibleAdapter.MODE_SINGLE | ||||
|                 adapter.mode = SelectableAdapter.Mode.SINGLE | ||||
|                 adapter.clearSelection() | ||||
|             } | ||||
|         } | ||||
| @@ -205,7 +205,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|     override fun onItemClick(position: Int): Boolean { | ||||
|         // If the action mode is created and the position is valid, toggle the selection. | ||||
|         val item = adapter.getItem(position) ?: return false | ||||
|         if (adapter.mode == FlexibleAdapter.MODE_MULTI) { | ||||
|         if (adapter.mode == SelectableAdapter.Mode.MULTI) { | ||||
|             toggleSelection(position) | ||||
|             return true | ||||
|         } else { | ||||
| @@ -244,4 +244,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att | ||||
|         controller.setSelection(item.manga, !adapter.isSelected(position)) | ||||
|         controller.invalidateActionMode() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -42,6 +42,7 @@ import io.realm.Realm | ||||
| import io.realm.RealmResults | ||||
| import kotlinx.android.synthetic.main.main_activity.* | ||||
| import kotlinx.android.synthetic.main.library_controller.view.* | ||||
| import kotlinx.android.synthetic.main.main_activity.* | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import timber.log.Timber | ||||
| @@ -219,17 +220,14 @@ class LibraryController( | ||||
|             drawer.addDrawerListener(it) | ||||
|         } | ||||
|         navView = view | ||||
|  | ||||
|         navView?.post { | ||||
|             if (isAttached && drawer.isDrawerOpen(navView)) | ||||
|                 drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) | ||||
|         } | ||||
|         drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) | ||||
|  | ||||
|         navView?.onGroupClicked = { group -> | ||||
|             when (group) { | ||||
|                 is LibraryNavigationView.FilterGroup -> onFilterChanged() | ||||
|                 is LibraryNavigationView.SortGroup -> onSortChanged() | ||||
|                 is LibraryNavigationView.DisplayGroup -> reattachAdapter() | ||||
|                 is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -316,7 +314,11 @@ class LibraryController( | ||||
|      */ | ||||
|     private fun onFilterChanged() { | ||||
|         presenter.requestFilterUpdate() | ||||
|         (activity as? AppCompatActivity)?.supportInvalidateOptionsMenu() | ||||
|         activity?.invalidateOptionsMenu() | ||||
|     } | ||||
|  | ||||
|     private fun onDownloadBadgeChanged(){ | ||||
|         presenter.requestDownloadBadgesUpdate() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.view.View | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import kotlinx.android.synthetic.main.catalogue_grid_item.view.* | ||||
|  | ||||
| /** | ||||
| @@ -19,29 +19,39 @@ import kotlinx.android.synthetic.main.catalogue_grid_item.view.* | ||||
| class LibraryGridHolder( | ||||
|         private val view: View, | ||||
|         private val adapter: FlexibleAdapter<*> | ||||
|  | ||||
| ) : LibraryHolder(view, adapter) { | ||||
|  | ||||
|     /** | ||||
|      * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this | ||||
|      * holder with the given manga. | ||||
|      * | ||||
|      * @param manga the manga to bind. | ||||
|      * @param item the manga item to bind. | ||||
|      */ | ||||
|     override fun onSetValues(manga: Manga) { | ||||
|     override fun onSetValues(item: LibraryItem) { | ||||
|         // Update the title of the manga. | ||||
|         view.title.text = manga.title | ||||
|         view.title.text = item.manga.title | ||||
|  | ||||
|         // Update the unread count and its visibility. | ||||
|         with(view.unread_text) { | ||||
|             visibility = if (manga.unread > 0) View.VISIBLE else View.GONE | ||||
|             text = manga.unread.toString() | ||||
|             visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE | ||||
|             text = item.manga.unread.toString() | ||||
|         } | ||||
|         // Update the download count and its visibility. | ||||
|         with(view.download_text) { | ||||
|             visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE | ||||
|             text = item.downloadCount.toString() | ||||
|         } | ||||
|         //set local visibility if its local manga | ||||
|         with(view.local_text) { | ||||
|             visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE | ||||
|         } | ||||
|  | ||||
|         // Update the cover. | ||||
|         Glide.clear(view.thumbnail) | ||||
|         Glide.with(view.context) | ||||
|                 .load(manga) | ||||
|                 .diskCacheStrategy(DiskCacheStrategy.RESULT) | ||||
|         GlideApp.with(view.context).clear(view.thumbnail) | ||||
|         GlideApp.with(view.context) | ||||
|                 .load(item.manga) | ||||
|                 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) | ||||
|                 .centerCrop() | ||||
|                 .into(view.thumbnail) | ||||
|     } | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| /** | ||||
|  * Generic class used to hold the displayed data of a manga in the library. | ||||
| @@ -21,8 +20,8 @@ abstract class LibraryHolder( | ||||
|      * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this | ||||
|      * holder with the given manga. | ||||
|      * | ||||
|      * @param manga the manga to bind. | ||||
|      * @param item the manga item to bind. | ||||
|      */ | ||||
|     abstract fun onSetValues(manga: Manga) | ||||
|     abstract fun onSetValues(item: LibraryItem) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,34 +1,38 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.view.Gravity | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.view.View | ||||
| import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| import android.widget.FrameLayout | ||||
| import com.f2prateek.rx.preferences.Preference | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.davidea.flexibleadapter.items.IFilterable | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.util.inflate | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.widget.AutofitRecyclerView | ||||
| import kotlinx.android.synthetic.main.catalogue_grid_item.view.* | ||||
|  | ||||
| class LibraryItem(val manga: Manga) : AbstractFlexibleItem<LibraryHolder>(), IFilterable { | ||||
|     // Temp metadata holder EH | ||||
| class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) : | ||||
|         AbstractFlexibleItem<LibraryHolder>(), IFilterable { | ||||
|     // Temp metadata holder (EXH) | ||||
|     @Volatile | ||||
|     var hasMetadata: Boolean? = null | ||||
|  | ||||
|     var downloadCount = -1 | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.catalogue_grid_item | ||||
|         return if (libraryAsList.getOrDefault()) | ||||
|             R.layout.catalogue_list_item | ||||
|         else | ||||
|             R.layout.catalogue_grid_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, | ||||
|                                   inflater: LayoutInflater, | ||||
|                                   parent: ViewGroup): LibraryHolder { | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder { | ||||
|         val parent = adapter.recyclerView | ||||
|         return if (parent is AutofitRecyclerView) { | ||||
|             val view = parent.inflate(R.layout.catalogue_grid_item).apply { | ||||
|             view.apply { | ||||
|                 val coverHeight = parent.itemWidth / 3 * 4 | ||||
|                 card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) | ||||
|                 gradient.layoutParams = FrameLayout.LayoutParams( | ||||
| @@ -36,7 +40,6 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem<LibraryHolder>(), IFi | ||||
|             } | ||||
|             LibraryGridHolder(view, adapter) | ||||
|         } else { | ||||
|             val view = parent.inflate(R.layout.catalogue_list_item) | ||||
|             LibraryListHolder(view, adapter) | ||||
|         } | ||||
|     } | ||||
| @@ -46,7 +49,7 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem<LibraryHolder>(), IFi | ||||
|                                 position: Int, | ||||
|                                 payloads: List<Any?>?) { | ||||
|  | ||||
|         holder.onSetValues(manga) | ||||
|         holder.onSetValues(this) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.view.View | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.engine.DiskCacheStrategy | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import jp.wasabeef.glide.transformations.CropCircleTransformation | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import kotlinx.android.synthetic.main.catalogue_list_item.view.* | ||||
|  | ||||
| /** | ||||
| @@ -27,16 +26,25 @@ class LibraryListHolder( | ||||
|      * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this | ||||
|      * holder with the given manga. | ||||
|      * | ||||
|      * @param manga the manga to bind. | ||||
|      * @param item the manga item to bind. | ||||
|      */ | ||||
|     override fun onSetValues(manga: Manga) { | ||||
|     override fun onSetValues(item: LibraryItem) { | ||||
|         // Update the title of the manga. | ||||
|         itemView.title.text = manga.title | ||||
|         itemView.title.text = item.manga.title | ||||
|  | ||||
|         // Update the unread count and its visibility. | ||||
|         with(itemView.unread_text) { | ||||
|             visibility = if (manga.unread > 0) View.VISIBLE else View.GONE | ||||
|             text = manga.unread.toString() | ||||
|             visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE | ||||
|             text = item.manga.unread.toString() | ||||
|         } | ||||
|         // Update the download count and its visibility. | ||||
|         with(itemView.download_text) { | ||||
|             visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE | ||||
|             text = "${item.downloadCount}" | ||||
|         } | ||||
|         //show local text badge if local manga | ||||
|         with(itemView.local_text) { | ||||
|             visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE | ||||
|         } | ||||
|  | ||||
|         // Create thumbnail onclick to simulate long click | ||||
| @@ -46,14 +54,14 @@ class LibraryListHolder( | ||||
|         } | ||||
|  | ||||
|         // Update the cover. | ||||
|         Glide.clear(itemView.thumbnail) | ||||
|         Glide.with(itemView.context) | ||||
|                 .load(manga) | ||||
|                 .diskCacheStrategy(DiskCacheStrategy.RESULT) | ||||
|         GlideApp.with(itemView.context).clear(itemView.thumbnail) | ||||
|         GlideApp.with(itemView.context) | ||||
|                 .load(item.manga) | ||||
|                 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) | ||||
|                 .centerCrop() | ||||
|                 .bitmapTransform(CropCircleTransformation(itemView.context)) | ||||
|                 .circleCrop() | ||||
|                 .dontAnimate() | ||||
|                 .into(itemView.thumbnail) | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A | ||||
|     /** | ||||
|      * List of groups shown in the view. | ||||
|      */ | ||||
|     private val groups = listOf(FilterGroup(), SortGroup(),  DisplayGroup()) | ||||
|     private val groups = listOf(FilterGroup(), SortGroup(),  DisplayGroup(), BadgeGroup()) | ||||
|  | ||||
|     /** | ||||
|      * Adapter instance. | ||||
| @@ -117,7 +117,9 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A | ||||
|  | ||||
|         private val unread = Item.MultiSort(R.string.action_filter_unread, this) | ||||
|  | ||||
|         override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total) | ||||
|         private val source = Item.MultiSort(R.string.manga_info_source_label, this) | ||||
|  | ||||
|         override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source) | ||||
|  | ||||
|         override val header = Item.Header(R.string.action_sort) | ||||
|  | ||||
| @@ -133,6 +135,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A | ||||
|             lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE | ||||
|             unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE | ||||
|             total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE | ||||
|             source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE | ||||
|         } | ||||
|  | ||||
|         override fun onItemClicked(item: Item) { | ||||
| @@ -153,6 +156,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A | ||||
|                 lastUpdated -> LibrarySort.LAST_UPDATED | ||||
|                 unread -> LibrarySort.UNREAD | ||||
|                 total -> LibrarySort.TOTAL | ||||
|                 source -> LibrarySort.SOURCE | ||||
|                 else -> throw Exception("Unknown sorting") | ||||
|             }) | ||||
|             preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) | ||||
| @@ -162,6 +166,23 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A | ||||
|  | ||||
|     } | ||||
|  | ||||
|     inner class BadgeGroup : Group { | ||||
|         private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this) | ||||
|         override val header = null | ||||
|         override val footer= null | ||||
|         override val items = listOf(downloadBadge) | ||||
|         override fun initModels() { | ||||
|             downloadBadge.checked = preferences.downloadBadge().getOrDefault() | ||||
|         } | ||||
|  | ||||
|         override fun onItemClicked(item: Item) { | ||||
|             item as Item.CheckboxGroup | ||||
|             item.checked = !item.checked | ||||
|             preferences.downloadBadge().set((item.checked)) | ||||
|             adapter.notifyItemChanged(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display group, to show the library as a list or a grid. | ||||
|      */ | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.util.Pair | ||||
| import com.hippo.unifile.UniFile | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| @@ -29,6 +28,16 @@ import java.io.IOException | ||||
| import java.io.InputStream | ||||
| import java.util.* | ||||
|  | ||||
| /** | ||||
|  * Class containing library information. | ||||
|  */ | ||||
| private data class Library(val categories: List<Category>, val mangaMap: LibraryMap) | ||||
|  | ||||
| /** | ||||
|  * Typealias for the library manga, using the category as keys, and list of manga as values. | ||||
|  */ | ||||
| private typealias LibraryMap = Map<Int, List<LibraryItem>> | ||||
|  | ||||
| /** | ||||
|  * Presenter of [LibraryController]. | ||||
|  */ | ||||
| @@ -53,6 +62,11 @@ class LibraryPresenter( | ||||
|      */ | ||||
|     private val filterTriggerRelay = BehaviorRelay.create(Unit) | ||||
|  | ||||
|     /** | ||||
|      * Relay used to apply the UI update to the last emission of the library. | ||||
|      */ | ||||
|     private val downloadTriggerRelay = BehaviorRelay.create(Unit) | ||||
|  | ||||
|     /** | ||||
|      * Relay used to apply the selected sorting method to the last emission of the library. | ||||
|      */ | ||||
| @@ -74,14 +88,15 @@ class LibraryPresenter( | ||||
|     fun subscribeLibrary() { | ||||
|         if (librarySubscription.isNullOrUnsubscribed()) { | ||||
|             librarySubscription = getLibraryObservable() | ||||
|                     .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()), | ||||
|                             { lib, _ -> lib.apply { setDownloadCount(mangaMap) } }) | ||||
|                     .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), | ||||
|                             { lib, _ -> Pair(lib.first, applyFilters(lib.second)) }) | ||||
|                             { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) }) | ||||
|                     .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), | ||||
|                             { lib, _ -> Pair(lib.first, applySort(lib.second)) }) | ||||
|                     .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) } | ||||
|                             { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) }) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribeLatestCache({ view, pair -> | ||||
|                         view.onNextLibraryUpdate(pair.first, pair.second) | ||||
|                     .subscribeLatestCache({ view, (categories, mangaMap) -> | ||||
|                         view.onNextLibraryUpdate(categories, mangaMap) | ||||
|                     }) | ||||
|         } | ||||
|     } | ||||
| @@ -91,7 +106,7 @@ class LibraryPresenter( | ||||
|      * | ||||
|      * @param map the map to filter. | ||||
|      */ | ||||
|     private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> { | ||||
|     private fun applyFilters(map: LibraryMap): LibraryMap { | ||||
|         // Cached list of downloaded manga directories given a source id. | ||||
|         val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>() | ||||
|  | ||||
| @@ -104,31 +119,36 @@ class LibraryPresenter( | ||||
|  | ||||
|         val filterCompleted = preferences.filterCompleted().getOrDefault() | ||||
|  | ||||
|         val filterFn: (Manga) -> Boolean = f@ { manga -> | ||||
|         val filterFn: (LibraryItem) -> Boolean = f@ { item -> | ||||
|             // Filter out manga without source. | ||||
|             val source = sourceManager.get(manga.source) ?: return@f false | ||||
|             val source = sourceManager.get(item.manga.source) ?: return@f false | ||||
|  | ||||
|             // Filter when there isn't unread chapters. | ||||
|             if (filterUnread && manga.unread == 0) { | ||||
|             if (filterUnread && item.manga.unread == 0) { | ||||
|                 return@f false | ||||
|             } | ||||
|  | ||||
|             if (filterCompleted && manga.status != SManga.COMPLETED) { | ||||
|             if (filterCompleted && item.manga.status != SManga.COMPLETED) { | ||||
|                 return@f false | ||||
|             } | ||||
|  | ||||
|             // Filter when the download directory doesn't exist or is null. | ||||
|             if (filterDownloaded) { | ||||
|                 // Don't bother with directory checking if download count has been set. | ||||
|                 if (item.downloadCount != -1) { | ||||
|                     return@f item.downloadCount > 0 | ||||
|                 } | ||||
|  | ||||
|                 // Get the directories for the source of the manga. | ||||
|                 val dirsForSource = mangaDirsForSource.getOrPut(source.id) { | ||||
|                     val sourceDir = downloadManager.findSourceDir(source) | ||||
|                     sourceDir?.listFiles()?.associateBy { it.name }.orEmpty() | ||||
|                 } | ||||
|  | ||||
|                 val mangaDirName = downloadManager.getMangaDirName(manga) | ||||
|                 val mangaDirName = downloadManager.getMangaDirName(item.manga) | ||||
|                 val mangaDir = dirsForSource[mangaDirName] ?: return@f false | ||||
|  | ||||
|                 val hasDirs = chapterDirectories.getOrPut(manga.id!!) { | ||||
|                 val hasDirs = chapterDirectories.getOrPut(item.manga.id!!) { | ||||
|                     mangaDir.listFiles()?.isNotEmpty() ?: false | ||||
|                 } | ||||
|                 if (!hasDirs) { | ||||
| @@ -141,12 +161,57 @@ class LibraryPresenter( | ||||
|         return map.mapValues { entry -> entry.value.filter(filterFn) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets downloaded chapter count to each manga. | ||||
|      * | ||||
|      * @param map the map of manga. | ||||
|      */ | ||||
|     private fun setDownloadCount(map: LibraryMap) { | ||||
|         if (!preferences.downloadBadge().getOrDefault()) { | ||||
|             // Unset download count if the preference is not enabled. | ||||
|             for ((_, itemList) in map) { | ||||
|                 for (item in itemList) { | ||||
|                     item.downloadCount = -1 | ||||
|                 } | ||||
|             } | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         // Cached list of downloaded manga directories given a source id. | ||||
|         val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>() | ||||
|  | ||||
|         // Cached list of downloaded chapter directories for a manga. | ||||
|         val chapterDirectories = mutableMapOf<Long, Int>() | ||||
|  | ||||
|         val downloadCountFn: (LibraryItem) -> Int = f@ { item -> | ||||
|             val source = sourceManager.get(item.manga.source) ?: return@f 0 | ||||
|  | ||||
|             // Get the directories for the source of the manga. | ||||
|             val dirsForSource = mangaDirsForSource.getOrPut(source.id) { | ||||
|                 val sourceDir = downloadManager.findSourceDir(source) | ||||
|                 sourceDir?.listFiles()?.associateBy { it.name }.orEmpty() | ||||
|             } | ||||
|             val mangaDirName = downloadManager.getMangaDirName(item.manga) | ||||
|             val mangaDir = dirsForSource[mangaDirName] ?: return@f 0 | ||||
|  | ||||
|             chapterDirectories.getOrPut(item.manga.id!!) { | ||||
|                 mangaDir.listFiles()?.size ?: 0 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for ((_, itemList) in map) { | ||||
|             for (item in itemList) { | ||||
|                 item.downloadCount = downloadCountFn(item) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Applies library sorting to the given map of manga. | ||||
|      * | ||||
|      * @param map the map to sort. | ||||
|      */ | ||||
|     private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> { | ||||
|     private fun applySort(map: LibraryMap): LibraryMap { | ||||
|         val sortingMode = preferences.librarySortingMode().getOrDefault() | ||||
|  | ||||
|         val lastReadManga by lazy { | ||||
| @@ -158,22 +223,27 @@ class LibraryPresenter( | ||||
|             db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } | ||||
|         } | ||||
|  | ||||
|         val sortFn: (Manga, Manga) -> Int = { manga1, manga2 -> | ||||
|         val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> | ||||
|             when (sortingMode) { | ||||
|                 LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title) | ||||
|                 LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title) | ||||
|                 LibrarySort.LAST_READ -> { | ||||
|                     // Get index of manga, set equal to list if size unknown. | ||||
|                     val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size | ||||
|                     val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size | ||||
|                     val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size | ||||
|                     val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size | ||||
|                     manga1LastRead.compareTo(manga2LastRead) | ||||
|                 } | ||||
|                 LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update) | ||||
|                 LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread) | ||||
|                 LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update) | ||||
|                 LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread) | ||||
|                 LibrarySort.TOTAL -> { | ||||
|                     val manga1TotalChapter = totalChapterManga[manga1.id!!] ?: 0 | ||||
|                     val mange2TotalChapter = totalChapterManga[manga2.id!!] ?: 0 | ||||
|                     val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0 | ||||
|                     val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0 | ||||
|                     manga1TotalChapter.compareTo(mange2TotalChapter) | ||||
|                 } | ||||
|                 LibrarySort.SOURCE -> { | ||||
|                     val source1Name = sourceManager.get(i1.manga.source)?.name ?: "" | ||||
|                     val source2Name = sourceManager.get(i2.manga.source)?.name ?: "" | ||||
|                     source1Name.compareTo(source2Name) | ||||
|                 } | ||||
|                 else -> throw Exception("Unknown sorting mode") | ||||
|             } | ||||
|         } | ||||
| @@ -191,7 +261,7 @@ class LibraryPresenter( | ||||
|      * | ||||
|      * @return an observable of the categories and its manga. | ||||
|      */ | ||||
|     private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> { | ||||
|     private fun getLibraryObservable(): Observable<Library> { | ||||
|         return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), | ||||
|                 { dbCategories, libraryManga -> | ||||
|                     val categories = if (libraryManga.containsKey(0)) | ||||
| @@ -200,7 +270,7 @@ class LibraryPresenter( | ||||
|                         dbCategories | ||||
|  | ||||
|                     this.categories = categories | ||||
|                     Pair(categories, libraryManga) | ||||
|                     Library(categories, libraryManga) | ||||
|                 }) | ||||
|     } | ||||
|  | ||||
| @@ -219,9 +289,12 @@ class LibraryPresenter( | ||||
|      * @return an observable containing a map with the category id as key and a list of manga as the | ||||
|      * value. | ||||
|      */ | ||||
|     private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> { | ||||
|     private fun getLibraryMangasObservable(): Observable<LibraryMap> { | ||||
|         val libraryAsList = preferences.libraryAsList() | ||||
|         return db.getLibraryMangas().asRxObservable() | ||||
|                 .map { list -> list.groupBy { it.category } } | ||||
|                 .map { list -> | ||||
|                     list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category } | ||||
|                 } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -231,6 +304,13 @@ class LibraryPresenter( | ||||
|         filterTriggerRelay.call(Unit) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Requests the library to have download badges added. | ||||
|      */ | ||||
|     fun requestDownloadBadgesUpdate() { | ||||
|         downloadTriggerRelay.call(Unit) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Requests the library to be sorted. | ||||
|      */ | ||||
|   | ||||
| @@ -7,4 +7,5 @@ object LibrarySort { | ||||
|     const val LAST_UPDATED = 2 | ||||
|     const val UNREAD = 3 | ||||
|     const val TOTAL = 4 | ||||
|     const val SOURCE = 5 | ||||
| } | ||||
| @@ -23,9 +23,8 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.TabbedController | ||||
| import eu.kanade.tachiyomi.ui.catalogue.CatalogueController | ||||
| import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController | ||||
| import eu.kanade.tachiyomi.ui.download.DownloadController | ||||
| import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryController | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController | ||||
| @@ -97,11 +96,10 @@ class MainActivity : BaseActivity() { | ||||
|                     R.id.nav_drawer_library -> setRoot(LibraryController(), id) | ||||
|                     R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) | ||||
|                     R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) | ||||
|                     R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) | ||||
|                     R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id) | ||||
|                     // --> EH | ||||
|                     R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id) | ||||
|                     // --> EXH | ||||
|                     R.id.nav_drawer_batch_add -> setRoot(BatchAddController(), id) | ||||
|                     // <-- EH | ||||
|                     // <-- EHX | ||||
|                     R.id.nav_drawer_downloads -> { | ||||
|                         router.pushController(RouterTransaction.with(DownloadController()) | ||||
|                                 .pushChangeHandler(FadeChangeHandler()) | ||||
| @@ -117,7 +115,7 @@ class MainActivity : BaseActivity() { | ||||
|             true | ||||
|         } | ||||
|  | ||||
|         val container = findViewById(R.id.controller_container) as ViewGroup | ||||
|         val container: ViewGroup = findViewById(R.id.controller_container) | ||||
|  | ||||
|         router = Conductor.attachRouter(this, container, savedInstanceState) | ||||
|         if (!router.hasRootController()) { | ||||
| @@ -347,4 +345,4 @@ class MainActivity : BaseActivity() { | ||||
|         const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,9 @@ import eu.davidea.viewholders.FlexibleViewHolder | ||||
| 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.util.getResourceColor | ||||
| import eu.kanade.tachiyomi.util.gone | ||||
| import eu.kanade.tachiyomi.util.setVectorCompat | ||||
| import kotlinx.android.synthetic.main.chapters_item.view.* | ||||
| import java.util.* | ||||
|  | ||||
| @@ -33,6 +35,9 @@ class ChapterHolder( | ||||
|             else -> chapter.name | ||||
|         } | ||||
|  | ||||
|         // Set the correct drawable for dropdown and update the tint to match theme. | ||||
|         view.chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) | ||||
|  | ||||
|         // Set correct text color | ||||
|         chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) | ||||
|         if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.view.View | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -27,11 +26,8 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem | ||||
|         return R.layout.chapters_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(adapter: FlexibleAdapter<*>, | ||||
|                                   inflater: LayoutInflater, | ||||
|                                   parent: ViewGroup): ChapterHolder { | ||||
|  | ||||
|         return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ChapterHolder { | ||||
|         return ChapterHolder(view, adapter as ChaptersAdapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder(adapter: FlexibleAdapter<*>, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user