# Conflicts:
#	README.md
#	app/build.gradle
#	app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
This commit is contained in:
NerdNumber9 2017-12-07 23:20:27 -05:00
commit 9af552c15a
79 changed files with 1972 additions and 1941 deletions

View File

@ -111,7 +111,7 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5' implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library // Android support library
final support_library_version = '27.0.1' final support_library_version = '27.0.2'
implementation "com.android.support:support-v4:$support_library_version" implementation "com.android.support:support-v4:$support_library_version"
implementation "com.android.support:appcompat-v7:$support_library_version" implementation "com.android.support:appcompat-v7:$support_library_version"
implementation "com.android.support:cardview-v7:$support_library_version" implementation "com.android.support:cardview-v7:$support_library_version"
@ -123,7 +123,7 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:multidex:1.0.1' implementation 'com.android.support:multidex:1.0.2'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
@ -154,14 +154,14 @@ dependencies {
// Disk // Disk
implementation 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.seven332:unifile:1.0.0' implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser // HTML parser
implementation 'org.jsoup:jsoup:1.10.2' implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling // Job scheduling
implementation 'com.evernote:android-job:1.2.0' implementation 'com.evernote:android-job:1.2.1'
implementation 'com.google.android.gms:play-services-gcm:11.6.0' implementation 'com.google.android.gms:play-services-gcm:11.6.2'
// Changelog // Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
@ -250,7 +250,7 @@ dependencies {
} }
buildscript { buildscript {
ext.kotlin_version = '1.1.61' ext.kotlin_version = '1.2.0'
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -268,3 +268,6 @@ kotlin {
coroutines 'enable' coroutines 'enable'
} }
} }
androidExtensions {
experimental = true
}

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.GET_TASKS"/> <uses-permission android:name="android.permission.GET_TASKS"/>

View File

@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource import eu.kanade.tachiyomi.util.syncChaptersWithSource
@ -41,6 +42,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
internal val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/** /**
* Version of parser * Version of parser
*/ */
@ -67,8 +73,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser = initParser() parser = initParser()
} }
private fun initParser(): Gson { private fun initParser(): Gson = when (version) {
return when (version) {
1 -> GsonBuilder().create() 1 -> GsonBuilder().create()
2 -> GsonBuilder() 2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
@ -79,7 +84,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.create() .create()
else -> throw Exception("Json version unknown") else -> throw Exception("Json version unknown")
} }
}
/** /**
* Backup the categories of library * Backup the categories of library
@ -300,6 +304,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val trackToUpdate = ArrayList<Track>() val trackToUpdate = ArrayList<Track>()
for (track in tracks) { for (track in tracks) {
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false var isInDatabase = false
for (dbTrack in dbTracks) { for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) { if (track.sync_id == dbTrack.sync_id) {
@ -319,6 +325,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
trackToUpdate.add(track) trackToUpdate.add(track)
} }
} }
}
// Update database // Update database
if (!trackToUpdate.isEmpty()) { if (!trackToUpdate.isEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
@ -361,32 +368,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* *
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? { internal fun getMangaFromDatabase(manga: Manga): Manga? =
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
}
/** /**
* Returns list containing manga from library * Returns list containing manga from library
* *
* @return [Manga] from library * @return [Manga] from library
*/ */
internal fun getFavoriteManga(): List<Manga> { internal fun getFavoriteManga(): List<Manga> =
return databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
}
/** /**
* Inserts manga and returns id * Inserts manga and returns id
* *
* @return id of [Manga], null if not found * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? { internal fun insertManga(manga: Manga): Long? =
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId() databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
}
/** /**
* Inserts list of chapters * Inserts list of chapters
*/ */
internal fun insertChapters(chapters: List<Chapter>) { private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking() databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
} }
@ -395,7 +399,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* *
* @return number of backups selected by user * @return number of backups selected by user
*/ */
fun numberOfBackups(): Int { fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
return preferences.numberOfBackups().getOrDefault()
}
} }

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning import eu.kanade.tachiyomi.util.isServiceRunning
@ -49,9 +50,8 @@ class BackupRestoreService : Service() {
* @param context the application context. * @param context the application context.
* @return true if the service is running, false otherwise. * @return true if the service is running, false otherwise.
*/ */
fun isRunning(context: Context): Boolean { private fun isRunning(context: Context): Boolean =
return context.isServiceRunning(BackupRestoreService::class.java) context.isServiceRunning(BackupRestoreService::class.java)
}
/** /**
* Starts a service to restore a backup from Json * Starts a service to restore a backup from Json
@ -113,7 +113,13 @@ class BackupRestoreService : Service() {
*/ */
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
lateinit var executor: ExecutorService /**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService
/** /**
* Method called when the service is created. It injects dependencies and acquire the wake lock. * Method called when the service is created. It injects dependencies and acquire the wake lock.
@ -142,9 +148,7 @@ class BackupRestoreService : Service() {
/** /**
* This method needs to be implemented, but it's not used/needed. * This method needs to be implemented, but it's not used/needed.
*/ */
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? = null
return null
}
/** /**
* Method called when the service receives an intent. * Method called when the service receives an intent.
@ -164,7 +168,7 @@ class BackupRestoreService : Service() {
subscription = Observable.using( subscription = Observable.using(
{ db.lowLevel().beginTransaction() }, { db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } }, { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } }) { executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) } .doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor)) .subscribeOn(Schedulers.from(executor))
@ -294,14 +298,14 @@ class BackupRestoreService : Service() {
val source = backupManager.sourceManager.get(manga.source) ?: return null val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) { return if (dbManga == null) {
// Manga not in database // Manga not in database
return mangaFetchObservable(source, manga, chapters, categories, history, tracks) mangaFetchObservable(source, manga, chapters, categories, history, tracks)
} else { // Manga in database } else { // Manga in database
// Copy information from manga already in database // Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga) backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information // Fetch rest of manga information
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks) mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
} }
} }
@ -327,14 +331,12 @@ class BackupRestoreService : Service() {
.map { manga } .map { manga }
} }
.doOnNext { .doOnNext {
// Restore categories restoreExtraForManga(it, categories, history, tracks)
backupManager.restoreCategoriesForManga(it, categories) }
.flatMap {
// Restore history trackingFetchObservable(it, tracks)
backupManager.restoreHistoryForManga(history) // Convert to the manga that contains new chapters.
.map { manga }
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
} }
.doOnCompleted { .doOnCompleted {
restoreProgress += 1 restoreProgress += 1
@ -356,14 +358,12 @@ class BackupRestoreService : Service() {
} }
} }
.doOnNext { .doOnNext {
// Restore categories restoreExtraForManga(it, categories, history, tracks)
backupManager.restoreCategoriesForManga(it, categories) }
.flatMap { manga ->
// Restore history trackingFetchObservable(manga, tracks)
backupManager.restoreHistoryForManga(history) // Convert to the manga that contains new chapters.
.map { manga }
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
} }
.doOnCompleted { .doOnCompleted {
restoreProgress += 1 restoreProgress += 1
@ -371,6 +371,17 @@ class BackupRestoreService : Service() {
} }
} }
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/** /**
* [Observable] that fetches chapter information * [Observable] that fetches chapter information
* *
@ -383,10 +394,33 @@ class BackupRestoreService : Service() {
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList<Chapter>(), emptyList<Chapter>()) Pair(emptyList(), emptyList())
} }
} }
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty()
}
}
}
/** /**
* Called to update dialog in [BackupConst] * Called to update dialog in [BackupConst]

View File

@ -85,9 +85,10 @@ class DownloadManager(context: Context) {
* *
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
* @param chapters the list of chapters to enqueue. * @param chapters the list of chapters to enqueue.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/ */
fun downloadChapters(manga: Manga, chapters: List<Chapter>) { fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
downloader.queueChapters(manga, chapters) downloader.queueChapters(manga, chapters, autoStart)
} }
/** /**

View File

@ -219,8 +219,9 @@ class Downloader(private val context: Context,
* *
* @param manga the manga of the chapters to download. * @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI { fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
// Called in background thread, the operation can be slow with SAF. // Called in background thread, the operation can be slow with SAF.
@ -261,9 +262,11 @@ class Downloader(private val context: Context,
} }
// Start downloader if needed // Start downloader if needed
if (autoStart) {
DownloadService.start(this@Downloader.context) DownloadService.start(this@Downloader.context)
} }
} }
}
/** /**
* Returns the observable which downloads a chapter. * Returns the observable which downloads a chapter.

View File

@ -333,7 +333,9 @@ class LibraryUpdateService(
val dbChapters = chapters.map { val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!! mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
} }
downloadManager.downloadChapters(manga, dbChapters) // We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, dbChapters, false)
} }
/** /**

View File

@ -118,7 +118,7 @@ class Mintmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex) val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()

View File

@ -118,7 +118,7 @@ class Readmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex) val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()

View File

@ -6,21 +6,39 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.*
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) { abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle),
LayoutContainer {
init {
addLifecycleListener(object : LifecycleListener() {
override fun postCreateView(controller: Controller, view: View) {
onViewCreated(view)
}
})
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
val view = inflateView(inflater, container) return inflateView(inflater, container)
onViewCreated(view, savedViewState) }
return view
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
} }
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View, savedViewState: Bundle?) { } open fun onViewCreated(view: View) { }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) { if (type.isEnter) {

View File

@ -5,6 +5,8 @@ import android.os.Build
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean { fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag) val controller = getControllerWithTag(tag)
@ -25,3 +27,9 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
} }
} }
} }
fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
}

View File

@ -30,7 +30,7 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
} }
@CallSuper @CallSuper
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
if (untilDestroySubscriptions.isUnsubscribed) { if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription() untilDestroySubscriptions = CompositeSubscription()
} }

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import kotlinx.android.extensions.LayoutContainer
abstract class BaseFlexibleViewHolder(view: View,
adapter: FlexibleAdapter<*>,
stickyHeader: Boolean = false) :
FlexibleViewHolder(view, adapter, stickyHeader), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
/** /**
* Adapter that holds the catalogue cards. * Adapter that holds the catalogue cards.
* *
* @param controller instance of [CatalogueMainController]. * @param controller instance of [CatalogueController].
*/ */
class CatalogueMainAdapter(val controller: CatalogueMainController) : class CatalogueAdapter(val controller: CatalogueController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) { FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
@ -31,7 +31,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/** /**
* Listener which should be called when user clicks browse. * Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueMainController] * Note: Should only be handled by [CatalogueController]
*/ */
interface OnBrowseClickListener { interface OnBrowseClickListener {
fun onBrowseClick(position: Int) fun onBrowseClick(position: Int)
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/** /**
* Listener which should be called when user clicks latest. * Listener which should be called when user clicks latest.
* Note: Should only be handled by [CatalogueMainController] * Note: Should only be handled by [CatalogueController]
*/ */
interface OnLatestClickListener { interface OnLatestClickListener {
fun onLatestClick(position: Int) fun onLatestClick(position: Int)

View File

@ -1,523 +1,231 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration import android.support.v7.widget.LinearLayoutManager
import android.os.Bundle import android.support.v7.widget.SearchView
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.widget.*
import android.view.* import android.view.*
import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_controller.view.* import kotlinx.android.synthetic.main.catalogue_main_controller.*
import kotlinx.android.synthetic.main.main_activity.* import uy.kohesive.injekt.Injekt
import rx.Observable import uy.kohesive.injekt.api.get
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/** /**
* Controller to manage the catalogues available in the app. * 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 [CataloguePresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
*/ */
open class CatalogueController(bundle: Bundle) : class CatalogueController : NucleusController<CataloguePresenter>(),
NucleusController<CataloguePresenter>(bundle), SourceLoginDialog.Listener,
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, CatalogueAdapter.OnBrowseClickListener,
FlexibleAdapter.EndlessScrollListener, CatalogueAdapter.OnLatestClickListener {
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/** /**
* Preferences helper. * Application preferences.
*/ */
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper = Injekt.get()
/** /**
* Adapter containing the list of manga from the catalogue. * Adapter containing sources.
*/ */
private var adapter: FlexibleAdapter<IFlexible<*>>? = null private var adapter : CatalogueAdapter? = null
/** /**
* Snackbar containing an error message when a request fails. * Called when controller is initialized.
*/ */
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* 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 { init {
// Enable the option menu
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? { override fun getTitle(): String? {
return presenter.source.name return applicationContext?.getString(R.string.label_catalogues)
} }
/**
* Create the [CataloguePresenter] used in controller.
*
* @return instance of [CataloguePresenter]
*/
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): CataloguePresenter {
return CataloguePresenter(args.getLong(SOURCE_ID_KEY)) return CataloguePresenter()
} }
/**
* 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 { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false) return inflater.inflate(R.layout.catalogue_main_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { /**
super.onViewCreated(view, savedViewState) * Called when the view is created
*
* @param view view of controller
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter, scroll listener and recycler views adapter = CatalogueAdapter(this)
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
navView?.setFilters(presenter.filterItems) // Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
view.progress?.visible() recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null adapter = null
snack = null super.onDestroyView(view)
recycler = null
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
// Inflate and prepare drawer super.onChangeStarted(handler, type)
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
this.navView = navView presenter.updateSources()
drawerListener = DrawerSwipeCloseListener(drawer, navView).also { }
drawer.addDrawerListener(it)
} }
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) /**
* Called when login dialog is closed, refreshes the adapter.
navView.onSearchClicked = { *
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() * @param source clicked item containing source information.
showProgressBar() */
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear() adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) presenter.loadSources()
}
} }
navView.onResetClicked = { /**
presenter.appliedFilters = FilterList() * Called when item is clicked
val newFilters = presenter.source.getFilterList() */
presenter.sourceFilters = newFilters override fun onItemClick(position: Int): Boolean {
navView.setFilters(presenter.filterItems) val item = adapter?.getItem(position) as? SourceItem ?: return false
} val source = item.source
return navView if (source is LoginSource && !source.isLogged()) {
} val dialog = SourceLoginDialog(source)
dialog.targetController = this
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { dialog.showDialog(router)
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = view.catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
view.catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else { } else {
(view.catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { // Open the catalogue view.
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() openCatalogue(source, BrowseCatalogueController(source))
.doOnNext { spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
else -> spanCount
} }
} return false
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
view.catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
} }
/**
* Called when browse is clicked in [CatalogueAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueAdapter]
*/
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: BrowseCatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(controller.withFadeTransaction())
}
/**
* 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) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu) // Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search menu // Initialize search option.
menu.findItem(R.id.action_search).apply { val searchItem = menu.findItem(R.id.action_search)
val searchView = actionView as SearchView val searchView = searchItem.actionView as SearchView
val query = presenter.query // Change hint to show global search.
if (!query.isBlank()) { searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents() // Create query listener which opens the global search view.
.skip(1) searchView.queryTextChangeEvents()
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted } .filter { it.isSubmitted }
.subscribeUntilDestroy {
searchViewSubscription?.unsubscribe() val query = it.queryText().toString()
searchViewSubscription = Observable.merge(writingObservable, submitObservable) router.pushController(CatalogueSearchController(query).withFadeTransaction())
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
} }
} }
/**
* 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode() // Initialize option to open catalogue settings.
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) } R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
return true return true
} }
/** /**
* Restarts the request with a new query. * Called to update adapter containing sources.
*
* @param newQuery the new query.
*/ */
private fun searchWithQuery(newQuery: String) { fun setSources(sources: List<IFlexible<*>>) {
// If text didn't change, do nothing adapter?.updateDataSet(sources)
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
} }
/** /**
* Called from the presenter when the network request is received. * Called to set the last used catalogue at the top of the view.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/ */
fun onAddPage(page: Int, mangas: List<CatalogueItem>) { fun setLastUsedSource(item: SourceItem?) {
val adapter = adapter ?: return adapter?.removeAllScrollableHeaders()
hideProgressBar() if (item != null) {
if (page == 1) { adapter?.addScrollableHeader(item)
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
Timber.e("onLoadMore")
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
} }
} }
override fun noMoreLoad(newItemsSize: Int) { class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* Returns a preference for the number of manga per row based on the current orientation.
*
* @return the preference.
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* 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): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
view?.progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
view?.progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(RouterTransaction.with(MangaController(item.manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity!!)
.items(resources?.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
} }

View File

@ -1,376 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager 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.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* Presenter of [CatalogueController]. * Presenter of [CatalogueController]
* 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.
*/ */
open class CataloguePresenter( class CataloguePresenter(
sourceId: Long, val sourceManager: SourceManager = Injekt.get(),
sourceManager: SourceManager = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get()
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : BasePresenter<CatalogueController>() {
/** /**
* Selected source. * Enabled sources.
*/ */
val source = sourceManager.get(sourceId) as CatalogueSource var sources = getEnabledSources()
/** /**
* Query from the view. * Subscription for retrieving enabled sources.
*/ */
var query = "" private var sourceSubscription: Subscription? = null
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
sourceFilters = source.getFilterList() // Load enabled and last used sources
loadSources()
if (savedState != null) { loadLastUsedSource()
query = savedState.getString(CataloguePresenter::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
super.onSave(state)
} }
/** /**
* Restarts the pager for the active source with the provided query and filters. * 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(CatalogueController::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(CatalogueController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
* *
* @param query the query. * @return list containing enabled sources.
* @param filters the current state of the filters (for search mode).
*/ */
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) { private fun getEnabledSources(): List<CatalogueSource> {
this.query = query val languages = preferences.enabledLanguages().getOrDefault()
this.appliedFilters = filters val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
subscribeToMangaInitializer() return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
// Create a new pager. .filterNot { it.id.toString() in hiddenCatalogues }
pager = createPager(query, filters) .sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
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(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
} }
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* 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
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* 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): 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) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
}
}
}
/**
* Get the default, and user categories.
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
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))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}
}
} }

View File

@ -1,16 +1,17 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.* import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
import java.util.* import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
BaseFlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) { fun bind(item: LangItem) {
itemView.title.text = when { title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source) item.code == "" -> itemView.context.getString(R.string.other_source)
else -> { else -> {
val locale = Locale(item.code) val locale = Locale(item.code)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue
class NoResultsException : Exception()

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas

View File

@ -1,44 +1,43 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.dpToPx import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.* import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) { class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) {
private val slice = Slice(itemView.card).apply { private val slice = Slice(card).apply {
setColor(adapter.cardBackground) setColor(adapter.cardBackground)
} }
init { init {
itemView.source_browse.setOnClickListener { source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition) adapter.browseClickListener.onBrowseClick(adapterPosition)
} }
itemView.source_latest.setOnClickListener { source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition) adapter.latestClickListener.onLatestClick(adapterPosition)
} }
} }
fun bind(item: SourceItem) { fun bind(item: SourceItem) {
val source = item.source val source = item.source
with(itemView) {
setCardEdges(item) setCardEdges(item)
// Set source name // Set source name
title.text = source.name title.text = source.name
// Set circle letter image. // Set circle letter image.
post { itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
} }
@ -51,7 +50,6 @@ class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHold
source_latest.visible() source_latest.visible()
} }
} }
}
private fun setCardEdges(item: SourceItem) { private fun setCardEdges(item: SourceItem) {
// Position of this item in its header. Defaults to 0 when header is null. // Position of this item in its header. Defaults to 0 when header is null.
@ -94,7 +92,7 @@ class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHold
} }
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = itemView.card val v = card
if (v.layoutParams is ViewGroup.MarginLayoutParams) { if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom) p.setMargins(left, top, right, bottom)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -26,7 +26,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
* Creates a new view holder for this item. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as CatalogueMainAdapter) return SourceHolder(view, adapter as CatalogueAdapter)
} }
/** /**

View File

@ -0,0 +1,520 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
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.widget.*
import android.view.*
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
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.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
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.catalogue_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
open class BrowseCatalogueController(bundle: Bundle) :
NucleusController<BrowseCataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Snackbar containing an error message when a request fails.
*/
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* 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 {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return presenter.source.name
}
override fun createPresenter(): BrowseCataloguePresenter {
return BrowseCataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
navView?.setFilters(presenter.filterItems)
progress?.visible()
}
override fun onDestroyView(view: View) {
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
snack = null
recycler = null
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
// Inflate and prepare drawer
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
navView.onResetClicked = {
presenter.appliedFilters = FilterList()
val newFilters = presenter.source.getFilterList()
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems)
}
return navView
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@BrowseCatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
else -> spanCount
}
}
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar()
if (page == 1) {
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0 until adapter.itemCount).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* Returns a preference for the number of manga per row based on the current orientation.
*
* @return the preference.
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* 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): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(MangaController(item.manga, true).withFadeTransaction())
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val activity = activity ?: return
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity)
.items(activity.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
}

View File

@ -0,0 +1,376 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.source.CatalogueSource
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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
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 [BrowseCatalogueController].
*/
open class BrowseCataloguePresenter(
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<BrowseCatalogueController>() {
/**
* Selected source.
*/
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
*/
var query = ""
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourceFilters = source.getFilterList()
if (savedState != null) {
query = savedState.getString(::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(::query.name, query)
super.onSave(state)
}
/**
* Restarts the pager for the active source with the provided query and filters.
*
* @param query the query.
* @param filters the current state of the filters (for search mode).
*/
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
this.query = query
this.appliedFilters = filters
subscribeToMangaInitializer()
// Create a new pager.
pager = createPager(query, filters)
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(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
}
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, BrowseCatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* 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
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* 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): 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) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
}
}
}
/**
* Get the default, and user categories.
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
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))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.*
/** /**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -27,16 +27,16 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
// Set manga title // Set manga title
view.title.text = manga.title title.text = manga.title
// Set alpha of thumbnail. // Set alpha of thumbnail.
view.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
setImage(manga) setImage(manga)
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
@ -44,7 +44,7 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress)) .into(StateImageViewTarget(thumbnail, progress))
} }
} }
} }

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/** /**
* Generic class used to hold the displayed data of a manga in the catalogue. * Generic class used to hold the displayed data of a manga in the catalogue.
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
*/ */
abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) : abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { BaseFlexibleViewHolder(view, adapter) {
/** /**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.*
/** /**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -29,14 +29,14 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
* @param manga the manga to bind. * @param manga the manga to bind.
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
view.title.text = manga.title title.text = manga.title
view.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor) title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
setImage(manga) setImage(manga)
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
@ -46,7 +46,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
.dontAnimate() .dontAnimate()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(view.thumbnail) .into(thumbnail)
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
class NoResultsException : Exception()

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View import android.view.View
import android.widget.ProgressBar import android.widget.ProgressBar

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.*
class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
: FlexibleViewHolder(view, adapter) { : BaseFlexibleViewHolder(view, adapter) {
init { init {
// Call onMangaClickListener when item is pressed. // Call onMangaClickListener when item is pressed.
@ -22,13 +22,13 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
} }
fun bind(manga: Manga) { fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title tvTitle.text = manga.title
setImage(manga) setImage(manga)
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage) GlideApp.with(itemView.context).clear(itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
@ -36,7 +36,7 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress)) .into(StateImageViewTarget(itemImage, progress))
} }
} }

View File

@ -4,15 +4,14 @@ import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller.*
/** /**
* This controller shows and manages the different search result in global search. * This controller shows and manages the different search result in global search.
@ -71,9 +70,7 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
*/ */
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
// Open MangaController. // Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true)) router.pushController(MangaController(manga, true).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
/** /**
@ -115,19 +112,16 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
* Called when the view is created * Called when the view is created
* *
* @param view view of controller * @param view view of controller
* @param savedViewState information from previous state.
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = CatalogueSearchAdapter(this) adapter = CatalogueSearchAdapter(this)
with(view) {
// Create recycler and set adapter. // Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter recycler.adapter = adapter
} }
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
adapter = null adapter = null

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.View import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.* import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
/** /**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards. * Holder that binds the [CatalogueSearchItem] containing catalogue cards.
@ -17,7 +17,8 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.vi
* @param view view of [CatalogueSearchItem] * @param view view of [CatalogueSearchItem]
* @param adapter instance of [CatalogueSearchAdapter] * @param adapter instance of [CatalogueSearchAdapter]
*/ */
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) { class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
BaseFlexibleViewHolder(view, adapter) {
/** /**
* Adapter containing manga from search results. * Adapter containing manga from search results.
@ -27,14 +28,12 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
private var lastBoundResults: List<CatalogueSearchCardItem>? = null private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init { init {
with(itemView) {
// Set layout horizontal. // Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp, nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
context.getResourceColor(android.R.attr.textColorHint)) view.context.getResourceColor(android.R.attr.textColorHint))
}
} }
/** /**
@ -46,7 +45,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
val source = item.source val source = item.source
val results = item.results val results = item.results
with(itemView) {
// Set Title witch country code if available. // Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
@ -69,7 +67,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
lastBoundResults = results lastBoundResults = results
} }
} }
}
/** /**
* Called from the presenter when a manga is initialized. * Called from the presenter when a manga is initialized.

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
super.onCreate(savedState) super.onCreate(savedState)
// Perform a search with previous or initial state // Perform a search with previous or initial state
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty()) search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty())
} }
override fun onDestroy() { override fun onDestroy() {
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query) state.putString(BrowseCataloguePresenter::query.name, query)
super.onSave(state) super.onSave(state)
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.catalogue.latest
import android.os.Bundle import android.os.Bundle
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
@ -6,19 +6,19 @@ import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
*/ */
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) { class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply { constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) putLong(SOURCE_ID_KEY, source.id)
}) })
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): BrowseCataloguePresenter {
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit BrowseCataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : BrowseCataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -1,238 +0,0 @@
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()
}

View File

@ -1,104 +0,0 @@
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
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
@ -15,7 +14,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.categories_controller.view.* import kotlinx.android.synthetic.main.categories_controller.*
/** /**
* Controller to manage the categories for the users' library. * Controller to manage the categories for the users' library.
@ -70,14 +69,12 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
* *
* @param view The view of this controller. * @param view The view of this controller.
* @param savedViewState The saved state of the view.
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) {
adapter = CategoryAdapter(this@CategoryController) adapter = CategoryAdapter(this@CategoryController)
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
adapter?.isHandleDragEnabled = true adapter?.isHandleDragEnabled = true
@ -87,7 +84,6 @@ class CategoryController : NucleusController<CategoryPresenter>(),
CategoryCreateDialog(this@CategoryController).showDialog(router, null) CategoryCreateDialog(this@CategoryController).showDialog(router, null)
} }
} }
}
/** /**
* Called when the view is being destroyed. Used to release references and remove callbacks. * Called when the view is being destroyed. Used to release references and remove callbacks.
@ -95,12 +91,12 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* @param view The view of this controller. * @param view The view of this controller.
*/ */
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
// Manually call callback to delete categories if required // Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null undoHelper = null
actionMode = null actionMode = null
adapter = null adapter = null
super.onDestroyView(view)
} }
/** /**

View File

@ -1,14 +1,10 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.graphics.Color
import android.graphics.Typeface
import android.view.View import android.view.View
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.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.* import kotlinx.android.synthetic.main.categories_item.*
/** /**
* Holder used to display category items. * Holder used to display category items.
@ -16,16 +12,16 @@ import kotlinx.android.synthetic.main.categories_item.view.*
* @param view The view used by category items. * @param view The view used by category items.
* @param adapter The adapter containing this holder. * @param adapter The adapter containing this holder.
*/ */
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) { class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleViewHolder(view, adapter) {
init { init {
// Create round letter image onclick to simulate long click // Create round letter image onclick to simulate long click
itemView.image.setOnClickListener { image.setOnClickListener {
// Simulate long click on this view to enter selection mode // Simulate long click on this view to enter selection mode
onLongClick(view) onLongClick(view)
} }
setDragHandleView(itemView.reorder) setDragHandleView(reorder)
} }
/** /**
@ -35,11 +31,11 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
*/ */
fun bind(category: Category) { fun bind(category: Category) {
// Set capitalized title. // Set capitalized title.
itemView.title.text = category.name.capitalize() title.text = category.name.capitalize()
// Update circle letter image. // Update circle letter image.
itemView.post { itemView.post {
itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false)) image.setImageDrawable(image.getRound(category.name.take(1).toUpperCase(),false))
} }
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.* import android.view.*
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -8,7 +7,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import kotlinx.android.synthetic.main.download_controller.view.* import kotlinx.android.synthetic.main.download_controller.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -52,21 +51,19 @@ class DownloadController : NucleusController<DownloadPresenter>() {
return resources?.getString(R.string.label_download_queue) return resources?.getString(R.string.label_download_queue)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
// Check if download queue is empty and update information accordingly. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()
// Initialize adapter. // Initialize adapter.
adapter = DownloadAdapter() adapter = DownloadAdapter()
with(view) {
recycler.adapter = adapter recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size. // Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
}
// Suscribe to changes // Suscribe to changes
DownloadService.runningRelay DownloadService.runningRelay
@ -83,12 +80,12 @@ class DownloadController : NucleusController<DownloadPresenter>() {
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
for (subscription in progressSubscriptions.values) { for (subscription in progressSubscriptions.values) {
subscription.unsubscribe() subscription.unsubscribe()
} }
progressSubscriptions.clear() progressSubscriptions.clear()
adapter = null adapter = null
super.onDestroyView(view)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -232,20 +229,18 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* @return the holder of the download or null if it's not bound. * @return the holder of the download or null if it's not bound.
*/ */
private fun getHolder(download: Download): DownloadHolder? { private fun getHolder(download: Download): DownloadHolder? {
val recycler = view?.recycler ?: return null return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
} }
/** /**
* Set information view when queue is empty * Set information view when queue is empty
*/ */
private fun setInformationView() { private fun setInformationView() {
val emptyView = view?.empty_view ?: return
if (presenter.downloadQueue.isEmpty()) { if (presenter.downloadQueue.isEmpty()) {
emptyView.show(R.drawable.ic_file_download_black_128dp, empty_view?.show(R.drawable.ic_file_download_black_128dp,
R.string.information_no_downloads) R.string.information_no_downloads)
} else { } else {
emptyView.hide() empty_view?.hide()
} }
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.download_item.view.* import kotlinx.android.synthetic.main.download_item.view.*
/** /**
@ -12,7 +12,7 @@ import kotlinx.android.synthetic.main.download_item.view.*
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @constructor creates a new download holder. * @constructor creates a new download holder.
*/ */
class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) { class DownloadHolder(private val view: View) : BaseViewHolder(view) {
private lateinit var download: Download private lateinit var download: Download

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -26,6 +26,8 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
} }
} }
private var boundViews = arrayListOf<View>()
/** /**
* Creates a new view for this adapter. * Creates a new view for this adapter.
* *
@ -45,6 +47,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/ */
override fun bindView(view: View, position: Int) { override fun bindView(view: View, position: Int) {
(view as LibraryCategoryView).onBind(categories[position]) (view as LibraryCategoryView).onBind(categories[position])
boundViews.add(view)
} }
/** /**
@ -55,6 +58,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/ */
override fun recycleView(view: View, position: Int) { override fun recycleView(view: View, position: Int) {
(view as LibraryCategoryView).onRecycle() (view as LibraryCategoryView).onRecycle()
boundViews.remove(view)
} }
/** /**
@ -85,4 +89,15 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
return if (index == -1) POSITION_NONE else index return if (index == -1) POSITION_NONE else index
} }
/**
* Called when the view of this adapter is being destroyed.
*/
fun onDestroy() {
for (view in boundViews) {
if (view is LibraryCategoryView) {
view.unsubscribe()
}
}
}
} }

View File

@ -124,12 +124,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onRecycle() { fun onRecycle() {
adapter.setItems(emptyList()) adapter.setItems(emptyList())
adapter.clearSelection() adapter.clearSelection()
subscriptions.clear() unsubscribe()
} }
override fun onDetachedFromWindow() { fun unsubscribe() {
subscriptions.clear() subscriptions.clear()
super.onDetachedFromWindow()
} }
/** /**

View File

@ -14,8 +14,6 @@ import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v4.view.pageSelections import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
@ -30,6 +28,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@ -41,7 +40,7 @@ import exh.metadata.models.SearchableGalleryMetadata
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.library_controller.view.* import kotlinx.android.synthetic.main.library_controller.*
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -108,14 +107,8 @@ class LibraryController(
private set private set
/** /**
* TabLayout of the categories. * Adapter of the view pager.
*/ */
private val tabs: TabLayout?
get() = activity?.tabs
private val drawer: DrawerLayout?
get() = activity?.drawer
private var adapter: LibraryAdapter? = null private var adapter: LibraryAdapter? = null
/** /**
@ -141,6 +134,7 @@ class LibraryController(
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
retainViewMode = RetainViewMode.RETAIN_DETACH
} }
override fun getTitle(): String? { override fun getTitle(): String? {
@ -165,13 +159,12 @@ class LibraryController(
} }
// <-- EH // <-- EH
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = LibraryAdapter(this) adapter = LibraryAdapter(this)
with(view) { library_pager.adapter = adapter
view_pager.adapter = adapter library_pager.pageSelections().skip(1).subscribeUntilDestroy {
view_pager.pageSelections().skip(1).subscribeUntilDestroy {
preferences.lastUsedCategory().set(it) preferences.lastUsedCategory().set(it)
activeCategory = it activeCategory = it
} }
@ -186,22 +179,22 @@ class LibraryController(
createActionModeIfNeeded() createActionModeIfNeeded()
} }
} }
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
if (type.isEnter) { if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager) activity?.tabs?.setupWithViewPager(library_pager)
presenter.subscribeLibrary() presenter.subscribeLibrary()
} }
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) adapter?.onDestroy()
adapter = null adapter = null
actionMode = null actionMode = null
tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null tabsVisibilitySubscription = null
super.onDestroyView(view)
// --> EH // --> EH
//Clean up realm //Clean up realm
@ -263,14 +256,14 @@ class LibraryController(
// Show empty view if needed // Show empty view if needed
if (mangaMap.isNotEmpty()) { if (mangaMap.isNotEmpty()) {
view.empty_view.hide() empty_view.hide()
} else { } else {
view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
} }
// Get the current active category. // Get the current active category.
val activeCat = if (adapter.categories.isNotEmpty()) val activeCat = if (adapter.categories.isNotEmpty())
view.view_pager.currentItem library_pager.currentItem
else else
activeCategory activeCategory
@ -278,14 +271,14 @@ class LibraryController(
adapter.categories = categories adapter.categories = categories
// Restore active category. // Restore active category.
view.view_pager.setCurrentItem(activeCat, false) library_pager.setCurrentItem(activeCat, false)
tabsVisibilityRelay.call(categories.size > 1) tabsVisibilityRelay.call(categories.size > 1)
// Delay the scroll position to allow the view to be properly measured. // Delay the scroll position to allow the view to be properly measured.
view.post { view.post {
if (isAttached) { if (isAttached) {
tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true) activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true)
} }
} }
@ -328,14 +321,13 @@ class LibraryController(
* Reattaches the adapter to the view pager to recreate fragments * Reattaches the adapter to the view pager to recreate fragments
*/ */
private fun reattachAdapter() { private fun reattachAdapter() {
val pager = view?.view_pager ?: return
val adapter = adapter ?: return val adapter = adapter ?: return
val position = pager.currentItem val position = library_pager.currentItem
adapter.recycle = false adapter.recycle = false
pager.adapter = adapter library_pager.adapter = adapter
pager.currentItem = position library_pager.currentItem = position
adapter.recycle = true adapter.recycle = true
} }
@ -361,7 +353,7 @@ class LibraryController(
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
if (!query.isNullOrEmpty()) { if (!query.isEmpty()) {
searchItem.expandActionView() searchItem.expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
searchView.clearFocus() searchView.clearFocus()
@ -395,15 +387,13 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_filter -> { R.id.action_filter -> {
navView?.let { drawer?.openDrawer(Gravity.END) } navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
} }
R.id.action_update_library -> { R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) } activity?.let { LibraryUpdateService.start(it) }
} }
R.id.action_edit_categories -> { R.id.action_edit_categories -> {
router.pushController(RouterTransaction.with(CategoryController()) router.pushController(CategoryController().withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
@ -459,9 +449,7 @@ class LibraryController(
// Notify the presenter a manga is being opened. // Notify the presenter a manga is being opened.
presenter.onOpenManga() presenter.onOpenManga()
router.pushController(RouterTransaction.with(MangaController(manga)) router.pushController(MangaController(manga).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler(false))
.popChangeHandler(FadeChangeHandler()))
} }
/** /**
@ -496,11 +484,11 @@ class LibraryController(
.toTypedArray() .toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
.showDialog(router, null) .showDialog(router)
} }
private fun showDeleteMangaDialog() { private fun showDeleteMangaDialog() {
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null) DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
@ -515,8 +503,6 @@ class LibraryController(
/** /**
* Changes the cover for the selected manga. * Changes the cover for the selected manga.
*
* @param mangas a list of selected manga.
*/ */
private fun changeSelectedCover() { private fun changeSelectedCover() {
val manga = selectedMangas.firstOrNull() ?: return val manga = selectedMangas.firstOrNull() ?: return

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,30 +30,28 @@ class LibraryGridHolder(
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// Update the title of the manga. // Update the title of the manga.
view.title.text = item.manga.title title.text = item.manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(view.unread_text) { with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString() text = item.manga.unread.toString()
} }
// Update the download count and its visibility. // Update the download count and its visibility.
with(view.download_text) { with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = item.downloadCount.toString() text = item.downloadCount.toString()
} }
//set local visibility if its local manga //set local visibility if its local manga
with(view.local_text) { local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Update the cover. // Update the cover.
GlideApp.with(view.context).clear(view.thumbnail) GlideApp.with(view.context).clear(thumbnail)
GlideApp.with(view.context) GlideApp.with(view.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(view.thumbnail) .into(thumbnail)
} }
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/** /**
* Generic class used to hold the displayed data of a manga in the library. * Generic class used to hold the displayed data of a manga in the library.
@ -14,7 +14,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
abstract class LibraryHolder( abstract class LibraryHolder(
view: View, view: View,
adapter: FlexibleAdapter<*> adapter: FlexibleAdapter<*>
) : FlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,38 +30,36 @@ class LibraryListHolder(
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// Update the title of the manga. // Update the title of the manga.
itemView.title.text = item.manga.title title.text = item.manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(itemView.unread_text) { with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString() text = item.manga.unread.toString()
} }
// Update the download count and its visibility. // Update the download count and its visibility.
with(itemView.download_text) { with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = "${item.downloadCount}" text = "${item.downloadCount}"
} }
//show local text badge if local manga //show local text badge if local manga
with(itemView.local_text) { local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Create thumbnail onclick to simulate long click // Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener { thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode // Simulate long click on this view to enter selection mode
onLongClick(itemView) onLongClick(itemView)
} }
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(itemView.thumbnail) GlideApp.with(itemView.context).clear(thumbnail)
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.circleCrop() .circleCrop()
.dontAnimate() .dontAnimate()
.into(itemView.thumbnail) .into(thumbnail)
} }
} }

View File

@ -13,17 +13,13 @@ import android.support.v4.widget.DrawerLayout
import android.support.v7.graphics.drawable.DrawerArrowDrawable import android.support.v7.graphics.drawable.DrawerArrowDrawable
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.* import com.bluelinelabs.conductor.*
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@ -96,19 +92,16 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_library -> setRoot(LibraryController(), id) R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id) R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
// --> EXH // --> EXH
R.id.nav_drawer_batch_add -> setRoot(BatchAddController(), id) R.id.nav_drawer_batch_add -> setRoot(BatchAddController(), id)
// <-- EHX // <-- EHX
R.id.nav_drawer_downloads -> { R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController()) router.pushController(DownloadController().withFadeTransaction())
.pushChangeHandler(FadeChangeHandler()) }
.popChangeHandler(FadeChangeHandler())) R.id.nav_drawer_settings -> {
router.pushController(SettingsMainController().withFadeTransaction())
} }
R.id.nav_drawer_settings ->
router.pushController(RouterTransaction.with(SettingsMainController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
} }
drawer.closeDrawer(GravityCompat.START) drawer.closeDrawer(GravityCompat.START)
@ -225,10 +218,7 @@ class MainActivity : BaseActivity() {
} }
private fun setRoot(controller: Controller, id: Int) { private fun setRoot(controller: Controller, id: Int) {
router.setRoot(RouterTransaction.with(controller) router.setRoot(controller.withFadeTransaction().tag(id.toString()))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler())
.tag(id.toString()))
} }
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.manga package eu.kanade.tachiyomi.ui.manga
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.TabLayout import android.support.design.widget.TabLayout
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
@ -32,7 +30,7 @@ import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.manga.track.TrackController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.manga_controller.view.* import kotlinx.android.synthetic.main.manga_controller.*
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -81,21 +79,19 @@ class MangaController : RxController, TabbedController {
return inflater.inflate(R.layout.manga_controller, container, false) return inflater.inflate(R.layout.manga_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
if (manga == null || source == null) return if (manga == null || source == null) return
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
with(view) {
adapter = MangaDetailAdapter() adapter = MangaDetailAdapter()
view_pager.offscreenPageLimit = 3 manga_pager.offscreenPageLimit = 3
view_pager.adapter = adapter manga_pager.adapter = adapter
if (!fromCatalogue) if (!fromCatalogue)
view_pager.currentItem = CHAPTERS_CONTROLLER manga_pager.currentItem = CHAPTERS_CONTROLLER
}
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
@ -106,7 +102,7 @@ class MangaController : RxController, TabbedController {
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
if (type.isEnter) { if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager) activity?.tabs?.setupWithViewPager(manga_pager)
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
} }
} }

View File

@ -2,41 +2,41 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.chapters_item.view.* import kotlinx.android.synthetic.main.chapters_item.*
import java.util.* import java.util.*
class ChapterHolder( class ChapterHolder(
private val view: View, private val view: View,
private val adapter: ChaptersAdapter private val adapter: ChaptersAdapter
) : FlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
init { init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is // We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the // correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown. // PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
} }
fun bind(item: ChapterItem, manga: Manga) = with(view) { fun bind(item: ChapterItem, manga: Manga) {
val chapter = item.chapter val chapter = item.chapter
chapter_title.text = when (manga.displayMode) { chapter_title.text = when (manga.displayMode) {
Manga.DISPLAY_NUMBER -> { Manga.DISPLAY_NUMBER -> {
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
context.getString(R.string.display_mode_chapter, number) itemView.context.getString(R.string.display_mode_chapter, number)
} }
else -> chapter.name else -> chapter.name
} }
// Set the correct drawable for dropdown and update the tint to match theme. // 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)) chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set correct text color // Set correct text color
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
@ -53,14 +53,14 @@ class ChapterHolder(
chapter_scanlator.text = chapter.scanlator chapter_scanlator.text = chapter.scanlator
//allow longer titles if there is no scanlator (most sources) //allow longer titles if there is no scanlator (most sources)
if (chapter_scanlator.text.isNullOrBlank()) { if (chapter_scanlator.text.isNullOrBlank()) {
chapter_title.setMaxLines(2) chapter_title.maxLines = 2
chapter_scanlator.gone() chapter_scanlator.gone()
} else { } else {
chapter_title.setMaxLines(1) chapter_title.maxLines = 1
} }
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
context.getString(R.string.chapter_progress, chapter.last_page_read + 1) itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
} else { } else {
"" ""
} }
@ -68,7 +68,7 @@ class ChapterHolder(
notifyStatus(item.status) notifyStatus(item.status)
} }
fun notifyStatus(status: Int) = with(view.download_text) { fun notifyStatus(status: Int) = with(download_text) {
when (status) { when (status) {
Download.QUEUE -> setText(R.string.chapter_queued) Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading) Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -4,7 +4,6 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
@ -26,7 +25,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.getCoordinates import eu.kanade.tachiyomi.util.getCoordinates
import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.chapters_controller.view.* import kotlinx.android.synthetic.main.chapters_controller.*
import timber.log.Timber import timber.log.Timber
class ChaptersController : NucleusController<ChaptersPresenter>(), class ChaptersController : NucleusController<ChaptersPresenter>(),
@ -69,18 +68,17 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
return inflater.inflate(R.layout.chapters_controller, container, false) return inflater.inflate(R.layout.chapters_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
// Init RecyclerView and adapter // Init RecyclerView and adapter
adapter = ChaptersAdapter(this, view.context) adapter = ChaptersAdapter(this, view.context)
with(view) {
recycler.adapter = adapter recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
adapter?.fastScroller = view.fast_scroller adapter?.fastScroller = fast_scroller
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
@ -100,26 +98,25 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
openChapter(item.chapter) openChapter(item.chapter)
} }
} else { } else {
context.toast(R.string.no_next_chapter) view.context.toast(R.string.no_next_chapter)
}
} }
} }
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
actionMode = null actionMode = null
super.onDestroyView(view)
} }
override fun onActivityResumed(activity: Activity) { override fun onActivityResumed(activity: Activity) {
val view = view ?: return if (view == null) return
// Check if animation view is visible // Check if animation view is visible
if (view.reveal_view.visibility == View.VISIBLE) { if (reveal_view.visibility == View.VISIBLE) {
// Show the unReveal effect // Show the unReveal effect
val coordinates = view.fab.getCoordinates() val coordinates = fab.getCoordinates()
view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
} }
super.onActivityResumed(activity) super.onActivityResumed(activity)
} }
@ -213,16 +210,16 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
} }
fun fetchChaptersFromSource() { fun fetchChaptersFromSource() {
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
presenter.fetchChaptersFromSource() presenter.fetchChaptersFromSource()
} }
fun onFetchChaptersDone() { fun onFetchChaptersDone() {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
} }
fun onFetchChaptersError(error: Throwable) { fun onFetchChaptersError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
activity?.toast(error.message) activity?.toast(error.message)
} }
@ -231,7 +228,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
} }
private fun getHolder(chapter: Chapter): ChapterHolder? { private fun getHolder(chapter: Chapter): ChapterHolder? {
return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
} }
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
@ -365,7 +362,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)
if (view != null && !presenter.manga.favorite) { if (view != null && !presenter.manga.favorite) {
view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_add) { setAction(R.string.action_add) {
presenter.addToLibrary() presenter.addToLibrary()
} }

View File

@ -39,7 +39,7 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.manga_info_controller.view.* import kotlinx.android.synthetic.main.manga_info_controller.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
@ -71,10 +71,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
return inflater.inflate(R.layout.manga_info_controller, container, false) return inflater.inflate(R.layout.manga_info_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) {
// Set onclickListener to toggle favorite when FAB clicked. // Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
@ -82,8 +81,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
} }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.manga_info, menu) inflater.inflate(R.menu.manga_info, menu)
} }
@ -124,7 +121,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
*/ */
private fun setMangaInfo(manga: Manga, source: Source?) { private fun setMangaInfo(manga: Manga, source: Source?) {
val view = view ?: return val view = view ?: return
with(view) {
// Update artist TextView. // Update artist TextView.
manga_artist.text = manga.artist manga_artist.text = manga.artist
@ -155,14 +152,14 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set cover if it wasn't already. // Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(manga_cover) .into(manga_cover)
if (backdrop != null) { if (backdrop != null) {
GlideApp.with(context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
@ -170,7 +167,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
} }
} }
} }
}
/** /**
* Update chapter count TextView. * Update chapter count TextView.
@ -178,7 +174,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param count number of chapters. * @param count number of chapters.
*/ */
fun setChapterCount(count: Float) { fun setChapterCount(count: Float) {
view?.manga_chapters?.text = DecimalFormat("#.#").format(count) manga_chapters?.text = DecimalFormat("#.#").format(count)
} }
/** /**
@ -243,7 +239,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
private fun setFavoriteDrawable(isFavorite: Boolean) { private fun setFavoriteDrawable(isFavorite: Boolean) {
// Set the Favorite drawable to the correct one. // Set the Favorite drawable to the correct one.
// Border drawable if false, filled drawable if true. // Border drawable if false, filled drawable if true.
view?.fab_favorite?.setImageResource(if (isFavorite) fab_favorite?.setImageResource(if (isFavorite)
R.drawable.ic_bookmark_white_24dp R.drawable.ic_bookmark_white_24dp
else else
R.drawable.ic_bookmark_border_white_24dp) R.drawable.ic_bookmark_border_white_24dp)
@ -279,7 +275,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param value whether it should be refreshing or not. * @param value whether it should be refreshing or not.
*/ */
private fun setRefreshing(value: Boolean) { private fun setRefreshing(value: Boolean) {
view?.swipe_refresh?.isRefreshing = value swipe_refresh?.isRefreshing = value
} }
/** /**

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.manga.track package eu.kanade.tachiyomi.ui.manga.track
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -11,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.track_controller.view.* import kotlinx.android.synthetic.main.track_controller.*
class TrackController : NucleusController<TrackPresenter>(), class TrackController : NucleusController<TrackPresenter>(),
TrackAdapter.OnRowClickListener, TrackAdapter.OnRowClickListener,
@ -35,8 +34,8 @@ class TrackController : NucleusController<TrackPresenter>(),
return inflater.inflate(R.layout.track_controller, container, false) return inflater.inflate(R.layout.track_controller, container, false)
} }
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
adapter = TrackAdapter(this) adapter = TrackAdapter(this)
with(view) { with(view) {
@ -48,14 +47,14 @@ class TrackController : NucleusController<TrackPresenter>(),
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
super.onDestroyView(view)
} }
fun onNextTrackings(trackings: List<TrackItem>) { fun onNextTrackings(trackings: List<TrackItem>) {
val atLeastOneLink = trackings.any { it.track != null } val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings adapter?.items = trackings
view?.swipe_refresh?.isEnabled = atLeastOneLink swipe_refresh?.isEnabled = atLeastOneLink
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
} }
@ -73,11 +72,11 @@ class TrackController : NucleusController<TrackPresenter>(),
} }
fun onRefreshDone() { fun onRefreshDone() {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
} }
fun onRefreshError(error: Throwable) { fun onRefreshError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false swipe_refresh?.isRefreshing = false
activity?.toast(error.message) activity?.toast(error.message)
} }
@ -109,17 +108,17 @@ class TrackController : NucleusController<TrackPresenter>(),
override fun setStatus(item: TrackItem, selection: Int) { override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection) presenter.setStatus(item, selection)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
override fun setScore(item: TrackItem, score: Int) { override fun setScore(item: TrackItem, score: Int) {
presenter.setScore(item, score) presenter.setScore(item, score)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
presenter.setLastChapterRead(item, chaptersRead) presenter.setLastChapterRead(item, chaptersRead)
view?.swipe_refresh?.isRefreshing = true swipe_refresh?.isRefreshing = true
} }
private companion object { private companion object {

View File

@ -1,29 +1,29 @@
package eu.kanade.tachiyomi.ui.manga.track package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.track_item.view.* import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.track_item.*
class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) { class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
init { init {
val listener = adapter.rowClickListener val listener = adapter.rowClickListener
view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun bind(item: TrackItem) = with(itemView) { fun bind(item: TrackItem) {
val track = item.track val track = item.track
track_logo.setImageResource(item.service.getLogo()) track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor()) logo.setBackgroundColor(item.service.getLogoColor())
if (track != null) { if (track != null) {
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
track_title.setAllCaps(false) track_title.setAllCaps(false)
track_title.text = track.title track_title.text = track.title
track_chapters.text = "${track.last_chapter_read}/" + track_chapters.text = "${track.last_chapter_read}/" +
@ -31,7 +31,7 @@ class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(v
track_status.text = item.service.getStatus(track.status) track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
} else { } else {
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
track_title.setText(R.string.action_edit) track_title.setText(R.string.action_edit)
track_chapters.text = "" track_chapters.text = ""
track_score.text = "" track_score.text = ""

View File

@ -118,7 +118,7 @@ abstract class PagerReader : BaseReader() {
this.pager = pager.apply { this.pager = pager.apply {
setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
setOffscreenPageLimit(1) setOffscreenPageLimit(1)
setId(R.id.view_pager) setId(R.id.reader_pager)
setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener { setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener {
override fun onFirstPageOutEvent() { override fun onFirstPageOutEvent() {
readerActivity.requestPreviousChapter() readerActivity.requestPreviousChapter()

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,10 +10,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.reader_webtoon_item.view.* import kotlinx.android.synthetic.main.reader_webtoon_item.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder. * @constructor creates a new webtoon holder.
*/ */
class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) : class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) :
RecyclerView.ViewHolder(view) { BaseViewHolder(view) {
/** /**
* Page of a chapter. * Page of a chapter.
@ -54,7 +54,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
private var decodeErrorLayout: View? = null private var decodeErrorLayout: View? = null
init { init {
with(view.image_view) { with(image_view) {
setMaxTileSize(readerActivity.maxBitmapSize) setMaxTileSize(readerActivity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
@ -78,11 +78,11 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
}) })
} }
view.progress_container.layoutParams = FrameLayout.LayoutParams( progress_container.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, webtoonReader.screenHeight) MATCH_PARENT, webtoonReader.screenHeight)
view.setOnTouchListener(adapter.touchListener) view.setOnTouchListener(adapter.touchListener)
view.retry_button.setOnTouchListener { _, event -> retry_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
readerActivity.presenter.retryPage(page) readerActivity.presenter.retryPage(page)
} }
@ -111,9 +111,9 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
(view as ViewGroup).removeView(it) (view as ViewGroup).removeView(it)
decodeErrorLayout = null decodeErrorLayout = null
} }
view.image_view.recycle() image_view.recycle()
view.image_view.visibility = View.GONE image_view.visibility = View.GONE
view.progress_container.visibility = View.VISIBLE progress_container.visibility = View.VISIBLE
} }
/** /**
@ -150,7 +150,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { progress -> .subscribe { progress ->
view.progress_text.text = if (progress > 0) { progress_text.text = if (progress > 0) {
view.context.getString(R.string.download_progress, progress) view.context.getString(R.string.download_progress, progress)
} else { } else {
view.context.getString(R.string.downloading) view.context.getString(R.string.downloading)
@ -279,14 +279,14 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* Called when the image is decoded and going to be displayed. * Called when the image is decoded and going to be displayed.
*/ */
private fun onImageDecoded() { private fun onImageDecoded() {
view.progress_container.visibility = View.GONE progress_container.visibility = View.GONE
} }
/** /**
* Called when the image fails to decode. * Called when the image fails to decode.
*/ */
private fun onImageDecodeError() { private fun onImageDecodeError() {
view.progress_container.visibility = View.GONE progress_container.visibility = View.GONE
val page = page ?: return val page = page ?: return
if (decodeErrorLayout != null || !webtoonReader.isAdded) return if (decodeErrorLayout != null || !webtoonReader.isAdded) return

View File

@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.recent_chapters_item.view.* import kotlinx.android.synthetic.main.recent_chapters_item.*
/** /**
* Holder that contains chapter item * Holder that contains chapter item
@ -22,7 +22,7 @@ import kotlinx.android.synthetic.main.recent_chapters_item.view.*
* @constructor creates a new recent chapter holder. * @constructor creates a new recent chapter holder.
*/ */
class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) : class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) :
FlexibleViewHolder(view, adapter) { BaseFlexibleViewHolder(view, adapter) {
/** /**
* Color of read chapter * Color of read chapter
@ -43,8 +43,8 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
// We need to post a Runnable to show the popup to make sure that the PopupMenu is // We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the // correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown. // PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
view.manga_cover.setOnClickListener { manga_cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition) adapter.coverClickListener.onCoverClick(adapterPosition)
} }
} }
@ -58,31 +58,31 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
this.item = item this.item = item
// Set chapter title // Set chapter title
view.chapter_title.text = item.chapter.name chapter_title.text = item.chapter.name
// Set manga title // Set manga title
view.manga_title.text = item.manga.title manga_title.text = item.manga.title
// Set the correct drawable for dropdown and update the tint to match theme. // Set the correct drawable for dropdown and update the tint to match theme.
view.chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set cover // Set cover
GlideApp.with(itemView.context).clear(itemView.manga_cover) GlideApp.with(itemView.context).clear(manga_cover)
if (!item.manga.thumbnail_url.isNullOrEmpty()) { if (!item.manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.circleCrop() .circleCrop()
.into(itemView.manga_cover) .into(manga_cover)
} }
// Check if chapter is read and set correct color // Check if chapter is read and set correct color
if (item.chapter.read) { if (item.chapter.read) {
view.chapter_title.setTextColor(readColor) chapter_title.setTextColor(readColor)
view.manga_title.setTextColor(readColor) manga_title.setTextColor(readColor)
} else { } else {
view.chapter_title.setTextColor(unreadColor) chapter_title.setTextColor(unreadColor)
view.manga_title.setTextColor(unreadColor) manga_title.setTextColor(unreadColor)
} }
// Set chapter status // Set chapter status
@ -94,7 +94,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
* *
* @param status download status * @param status download status
*/ */
fun notifyStatus(status: Int) = with(view.download_text) { fun notifyStatus(status: Int) = with(download_text) {
when (status) { when (status) {
Download.QUEUE -> setText(R.string.chapter_queued) Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading) Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.DividerItemDecoration import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.* import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.support.v4.widget.refreshes
import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -19,10 +16,11 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.recent_chapters_controller.view.* import kotlinx.android.synthetic.main.recent_chapters_controller.*
import timber.log.Timber import timber.log.Timber
/** /**
@ -65,16 +63,14 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
/** /**
* Called when view is created * Called when view is created
* @param view created view * @param view created view
* @param savedViewState status of saved sate
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) {
// Init RecyclerView and adapter // Init RecyclerView and adapter
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = layoutManager recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this@RecentChaptersController) adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter recycler.adapter = adapter
@ -85,22 +81,21 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
swipe_refresh.isEnabled = firstPos <= 0 swipe_refresh.isEnabled = firstPos <= 0
} }
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
swipe_refresh.refreshes().subscribeUntilDestroy { swipe_refresh.refreshes().subscribeUntilDestroy {
if (!LibraryUpdateService.isRunning(context)) { if (!LibraryUpdateService.isRunning(view.context)) {
LibraryUpdateService.start(context) LibraryUpdateService.start(view.context)
context.toast(R.string.action_update_library) view.context.toast(R.string.action_update_library)
} }
// It can be a very long operation, so we disable swipe refresh and show a toast. // It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false swipe_refresh.isRefreshing = false
} }
} }
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
actionMode = null actionMode = null
super.onDestroyView(view)
} }
/** /**
@ -180,11 +175,10 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
} }
override fun onUpdateEmptyView(size: Int) { override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) { if (size > 0) {
emptyView.hide() empty_view?.hide()
} else { } else {
emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
} }
} }
@ -201,7 +195,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
* @param download [Download] object containing download progress. * @param download [Download] object containing download progress.
*/ */
private fun getHolder(download: Download): RecentChapterHolder? { private fun getHolder(download: Download): RecentChapterHolder? {
return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
} }
/** /**
@ -260,9 +254,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
} }
fun openManga(chapter: RecentChapterItem) { fun openManga(chapter: RecentChapterItem) {
router.pushController(RouterTransaction.with(MangaController(chapter.manga)) router.pushController(MangaController(chapter.manga).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
/** /**

View File

@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.ui.recently_read package eu.kanade.tachiyomi.ui.recently_read
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.recently_read_controller.view.* import kotlinx.android.synthetic.main.recently_read_controller.*
/** /**
* Fragment that shows recently read manga. * Fragment that shows recently read manga.
@ -51,23 +49,20 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
* Called when view is created * Called when view is created
* *
* @param view created view * @param view created view
* @param savedViewState saved state of the view
*/ */
override fun onViewCreated(view: View, savedViewState: Bundle?) { override fun onViewCreated(view: View) {
super.onViewCreated(view, savedViewState) super.onViewCreated(view)
with(view) {
// Initialize adapter // Initialize adapter
recycler.layoutManager = LinearLayoutManager(context) recycler.layoutManager = LinearLayoutManager(view.context)
adapter = RecentlyReadAdapter(this@RecentlyReadController) adapter = RecentlyReadAdapter(this@RecentlyReadController)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
} }
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null adapter = null
super.onDestroyView(view)
} }
/** /**
@ -80,11 +75,10 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
} }
override fun onUpdateEmptyView(size: Int) { override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) { if (size > 0) {
emptyView.hide() empty_view.hide()
} else { } else {
emptyView.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga) empty_view.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga)
} }
} }
@ -108,9 +102,7 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
override fun onCoverClick(position: Int) { override fun onCoverClick(position: Int) {
val manga = adapter?.getItem(position)?.mch?.manga ?: return val manga = adapter?.getItem(position)?.mch?.manga ?: return
router.pushController(RouterTransaction.with(MangaController(manga)) router.pushController(MangaController(manga).withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
override fun removeHistory(manga: Manga, history: History, all: Boolean) { override fun removeHistory(manga: Manga, history: History, all: Boolean) {

View File

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.recently_read
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import kotlinx.android.synthetic.main.recently_read_item.view.* import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.recently_read_item.*
import java.util.* import java.util.*
/** /**
@ -21,18 +21,18 @@ import java.util.*
class RecentlyReadHolder( class RecentlyReadHolder(
view: View, view: View,
val adapter: RecentlyReadAdapter val adapter: RecentlyReadAdapter
) : FlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
init { init {
itemView.remove.setOnClickListener { remove.setOnClickListener {
adapter.removeClickListener.onRemoveClick(adapterPosition) adapter.removeClickListener.onRemoveClick(adapterPosition)
} }
itemView.resume.setOnClickListener { resume.setOnClickListener {
adapter.resumeClickListener.onResumeClick(adapterPosition) adapter.resumeClickListener.onResumeClick(adapterPosition)
} }
itemView.cover.setOnClickListener { cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition) adapter.coverClickListener.onCoverClick(adapterPosition)
} }
} }
@ -47,24 +47,24 @@ class RecentlyReadHolder(
val (manga, chapter, history) = item val (manga, chapter, history) = item
// Set manga title // Set manga title
itemView.manga_title.text = manga.title manga_title.text = manga.title
// Set source + chapter title // Set source + chapter title
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source) manga_source.text = itemView.context.getString(R.string.recent_manga_source)
.format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber) .format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber)
// Set last read timestamp title // Set last read timestamp title
itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read)) last_read.text = adapter.dateFormat.format(Date(history.last_read))
// Set cover // Set cover
GlideApp.with(itemView.context).clear(itemView.cover) GlideApp.with(itemView.context).clear(cover)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(itemView.cover) .into(cover)
} }
} }

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.app.Dialog import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import android.view.View import android.view.View
@ -22,8 +24,6 @@ import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import android.content.Intent
import android.net.Uri
class SettingsAboutController : SettingsController() { class SettingsAboutController : SettingsController() {
@ -71,7 +71,7 @@ class SettingsAboutController : SettingsController() {
} }
preference { preference {
title = "Discord" title = "Discord"
val url = "https://discord.gg/WrBkRk4" val url = "https://discord.gg/2dDQBv2"
summary = url summary = url
onClick { onClick {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
class SettingsMainController : SettingsController() { class SettingsMainController : SettingsController() {
@ -69,8 +68,6 @@ class SettingsMainController : SettingsController() {
} }
private fun navigateTo(controller: SettingsController) { private fun navigateTo(controller: SettingsController) {
router.pushController(RouterTransaction.with(controller) router.pushController(controller.withFadeTransaction())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
} }
} }

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.PreferenceGroup
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController" tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
android:id="@id/swipe_refresh" android:id="@id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@ -11,7 +11,7 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/catalogue_view" android:id="@+id/catalogue_view"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController">
<ProgressBar <ProgressBar
android:id="@+id/progress" android:id="@+id/progress"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.catalogue.CatalogueNavigationView <eu.kanade.tachiyomi.ui.catalogue.browse.CatalogueNavigationView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_view2" android:id="@+id/nav_view2"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -8,7 +8,7 @@
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/view_pager"/> android:id="@+id/library_pager"/>
<eu.kanade.tachiyomi.widget.EmptyView <eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view" android:id="@+id/empty_view"

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_pager" android:id="@+id/manga_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:background="?android:attr/colorBackground" />

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController" tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
android:id="@id/swipe_refresh" android:id="@id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<changelog bulletedList="true"> <changelog bulletedList="true">
<changelogversion versionName="v0.6.6" changeDate="">
<changelogtext>Backups now properly restore tracking information.</changelogtext>
<changelogtext>Fixed library view and its overflow menu visible in other screens.</changelogtext>
<changelogtext>Fixed updater's notification in Android O.</changelogtext>
<changelogtext>Fixed a crash when rotating the screen in the chapters view.</changelogtext>
<changelogtext>Improved peformance of the app when using a custom downloads directory.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.6.5" changeDate=""> <changelogversion versionName="v0.6.5" changeDate="">
<changelogtext>Added a download cache for faster navigation.</changelogtext> <changelogtext>Added a download cache for faster navigation.</changelogtext>

View File

@ -201,7 +201,7 @@
<string name="dialog_restoring_backup">Backup wird wiederhergestellt <string name="dialog_restoring_backup">Backup wird wiederhergestellt
%1$s zur Bibliothek hinzugefügt</string> %1$s zur Bibliothek hinzugefügt</string>
<string name="source_not_found">Quelle nicht gefunden</string> <string name="source_not_found">Quelle nicht gefunden</string>
<string name="dialog_restoring_source_not_found">Backup wird wiederhergestellt %1%s <string name="dialog_restoring_source_not_found">Backup wird wiederhergestellt %1$s
\nQuelle nicht gefunden</string> \nQuelle nicht gefunden</string>
<string name="backup_created">Backup erstellt</string> <string name="backup_created">Backup erstellt</string>
<string name="restore_completed">Wiederherstellen erfolgreich</string> <string name="restore_completed">Wiederherstellen erfolgreich</string>

View File

@ -3,4 +3,6 @@
<item name="catalogue_filter_sort_group" type="id"/> <item name="catalogue_filter_sort_group" type="id"/>
<item name="catalogue_filter_sort_item" type="id"/> <item name="catalogue_filter_sort_item" type="id"/>
<item name="reader_pager" type="id"/>
</resources> </resources>