# 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'
// 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:appcompat-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:multidex:1.0.1'
implementation 'com.android.support:multidex:1.0.2'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@ -154,14 +154,14 @@ dependencies {
// Disk
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.seven332:unifile:1.0.0'
implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser
implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling
implementation 'com.evernote:android-job:1.2.0'
implementation 'com.google.android.gms:play-services-gcm:11.6.0'
implementation 'com.evernote:android-job:1.2.1'
implementation 'com.google.android.gms:play-services-gcm:11.6.2'
// Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
@ -250,7 +250,7 @@ dependencies {
}
buildscript {
ext.kotlin_version = '1.1.61'
ext.kotlin_version = '1.2.0'
repositories {
mavenCentral()
}
@ -268,3 +268,6 @@ kotlin {
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.WAKE_LOCK" />
<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="android.permission.USE_FINGERPRINT" />
<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.preference.PreferencesHelper
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.SourceManager
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()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/**
* Version of parser
*/
@ -67,8 +73,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser = initParser()
}
private fun initParser(): Gson {
return when (version) {
private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
@ -79,7 +84,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.create()
else -> throw Exception("Json version unknown")
}
}
/**
* Backup the categories of library
@ -300,6 +304,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val trackToUpdate = ArrayList<Track>()
for (track in tracks) {
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
@ -319,6 +325,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
trackToUpdate.add(track)
}
}
}
// Update database
if (!trackToUpdate.isEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
@ -361,32 +368,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? {
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
}
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
internal fun getFavoriteManga(): List<Manga> {
return databaseHelper.getFavoriteMangas().executeAsBlocking()
}
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? {
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
}
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
internal fun insertChapters(chapters: List<Chapter>) {
private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
@ -395,7 +399,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return number of backups selected by user
*/
fun numberOfBackups(): Int {
return preferences.numberOfBackups().getOrDefault()
}
fun numberOfBackups(): Int = 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.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning
@ -49,9 +50,8 @@ class BackupRestoreService : Service() {
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return context.isServiceRunning(BackupRestoreService::class.java)
}
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java)
/**
* Starts a service to restore a backup from Json
@ -113,7 +113,13 @@ class BackupRestoreService : Service() {
*/
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.
@ -142,9 +148,7 @@ class BackupRestoreService : Service() {
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
@ -164,7 +168,7 @@ class BackupRestoreService : Service() {
subscription = Observable.using(
{ db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor))
@ -294,14 +298,14 @@ class BackupRestoreService : Service() {
val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) {
return if (dbManga == null) {
// Manga not in database
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
} else { // Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// 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 }
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
@ -356,14 +358,12 @@ class BackupRestoreService : Service() {
}
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
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
*
@ -383,10 +394,33 @@ class BackupRestoreService : Service() {
// If there's any error, return empty update and continue.
.onErrorReturn {
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]

View File

@ -85,9 +85,10 @@ class DownloadManager(context: Context) {
*
* @param manga the manga of the chapters.
* @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>) {
downloader.queueChapters(manga, chapters)
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
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 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
// 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
if (autoStart) {
DownloadService.start(this@Downloader.context)
}
}
}
/**
* Returns the observable which downloads a chapter.

View File

@ -333,7 +333,9 @@ class LibraryUpdateService(
val dbChapters = chapters.map {
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 trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"")
val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()

View File

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

View File

@ -6,21 +6,39 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
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 {
val view = inflateView(inflater, container)
onViewCreated(view, savedViewState)
return view
return inflateView(inflater, container)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
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) {
if (type.isEnter) {

View File

@ -5,6 +5,8 @@ import android.os.Build
import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean {
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
override fun onViewCreated(view: View, savedViewState: Bundle?) {
override fun onViewCreated(view: View) {
if (untilDestroySubscriptions.isUnsubscribed) {
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.items.IFlexible
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
/**
* 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) {
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.
* Note: Should only be handled by [CatalogueMainController]
* Note: Should only be handled by [CatalogueController]
*/
interface OnBrowseClickListener {
fun onBrowseClick(position: Int)
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/**
* 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 {
fun onLatestClick(position: Int)

View File

@ -1,523 +1,231 @@
package eu.kanade.tachiyomi.ui.catalogue
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.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
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.changehandler.FadeChangeHandler
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.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
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.view.*
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
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* 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) :
NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController,
class CatalogueController : NucleusController<CataloguePresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
CatalogueAdapter.OnBrowseClickListener,
CatalogueAdapter.OnLatestClickListener {
/**
* 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 {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
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 {
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 {
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 = FlexibleAdapter(null, this)
setupRecycler(view)
adapter = CatalogueAdapter(this)
navView?.setFilters(presenter.filterItems)
view.progress?.visible()
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
}
override fun onDestroyView(view: View) {
super.onDestroyView(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)
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
/**
* 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.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
presenter.loadSources()
}
}
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 = 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))
}
/**
* 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 {
(view.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@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
// Open the catalogue view.
openCatalogue(source, BrowseCatalogueController(source))
}
}
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
view.catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
return false
}
/**
* 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) {
inflater.inflate(R.menu.catalogue_list, menu)
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search menu
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.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)
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController(CatalogueSearchController(query).withFadeTransaction())
}
}
/**
* 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) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
// 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
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
* Called to update adapter containing sources.
*/
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)
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
* Called to set the last used catalogue at the top of the view.
*/
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 = 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
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
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..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"
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -1,376 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue
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.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
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
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(
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : 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 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
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourceFilters = source.getFilterList()
if (savedState != null) {
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)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* 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.
* @param filters the current state of the filters (for search mode).
* @return list containing enabled sources.
*/
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
this.query = query
this.appliedFilters = filters
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
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()
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
}
/**
* 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 eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
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) {
itemView.title.text = when {
title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source)
else -> {
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 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.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.view.View
import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
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)
}
init {
itemView.source_browse.setOnClickListener {
source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition)
}
itemView.source_latest.setOnClickListener {
source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition)
}
}
fun bind(item: SourceItem) {
val source = item.source
with(itemView) {
setCardEdges(item)
// Set source name
title.text = source.name
// Set circle letter image.
post {
itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
@ -51,7 +50,6 @@ class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHold
source_latest.visible()
}
}
}
private fun setCardEdges(item: SourceItem) {
// 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) {
val v = itemView.card
val v = card
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams
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 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.
*/
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 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.glide.GlideApp
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.
@ -27,16 +27,16 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
*/
override fun onSetValues(manga: Manga) {
// Set manga title
view.title.text = manga.title
title.text = manga.title
// 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)
}
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
@ -44,7 +44,7 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
.centerCrop()
.skipMemoryCache(true)
.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 eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
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.
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
* @param adapter the adapter handling this holder.
*/
abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
BaseFlexibleViewHolder(view, adapter) {
/**
* 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.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 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.glide.GlideApp
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.
@ -29,14 +29,14 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
view.title.text = manga.title
view.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
title.text = manga.title
title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
setImage(manga)
}
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
@ -46,7 +46,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
.dontAnimate()
.skipMemoryCache(true)
.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.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.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 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.widget.ProgressBar

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
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)
: FlexibleViewHolder(view, adapter) {
: BaseFlexibleViewHolder(view, adapter) {
init {
// Call onMangaClickListener when item is pressed.
@ -22,13 +22,13 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
}
fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title
tvTitle.text = manga.title
setImage(manga)
}
fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage)
GlideApp.with(itemView.context).clear(itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
@ -36,7 +36,7 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
.centerCrop()
.skipMemoryCache(true)
.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.SearchView
import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
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.
@ -71,9 +70,7 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
*/
override fun onMangaClick(manga: Manga) {
// Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga, true).withFadeTransaction())
}
/**
@ -115,19 +112,16 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
* 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)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = CatalogueSearchAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter
}
}
override fun onDestroyView(view: View) {
adapter = null

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
/**
* 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 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.
@ -27,14 +28,12 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init {
with(itemView) {
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
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 results = item.results
with(itemView) {
// Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
@ -69,7 +67,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
lastBoundResults = results
}
}
}
/**
* 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.online.LoginSource
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.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
super.onCreate(savedState)
// 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() {
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
state.putString(BrowseCataloguePresenter::query.name, query)
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.support.v4.widget.DrawerLayout
@ -6,19 +6,19 @@ import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
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 {
putLong(SOURCE_ID_KEY, source.id)
})
override fun createPresenter(): CataloguePresenter {
override fun createPresenter(): BrowseCataloguePresenter {
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.model.MangasPage
import eu.kanade.tachiyomi.ui.catalogue.Pager
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
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
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
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.ui.base.controller.NucleusController
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.
@ -70,14 +69,12 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* Called after view inflation. Used to initialize the view.
*
* @param view The view of this controller.
* @param savedViewState The saved state of the view.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
adapter = CategoryAdapter(this@CategoryController)
recycler.layoutManager = LinearLayoutManager(context)
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
@ -87,7 +84,6 @@ class CategoryController : NucleusController<CategoryPresenter>(),
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
}
}
}
/**
* 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.
*/
override fun onDestroyView(view: View) {
super.onDestroyView(view)
// Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
super.onDestroyView(view)
}
/**

View File

@ -1,14 +1,10 @@
package eu.kanade.tachiyomi.ui.category
import android.graphics.Color
import android.graphics.Typeface
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.ui.base.holder.BaseFlexibleViewHolder
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.
@ -16,16 +12,16 @@ import kotlinx.android.synthetic.main.categories_item.view.*
* @param view The view used by category items.
* @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 {
// Create round letter image onclick to simulate long click
itemView.image.setOnClickListener {
image.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(view)
}
setDragHandleView(itemView.reorder)
setDragHandleView(reorder)
}
/**
@ -35,11 +31,11 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
*/
fun bind(category: Category) {
// Set capitalized title.
itemView.title.text = category.name.capitalize()
title.text = category.name.capitalize()
// Update circle letter image.
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
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.*
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.source.model.Page
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.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -52,21 +51,19 @@ class DownloadController : NucleusController<DownloadPresenter>() {
return resources?.getString(R.string.label_download_queue)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Check if download queue is empty and update information accordingly.
setInformationView()
// Initialize adapter.
adapter = DownloadAdapter()
with(view) {
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(context)
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
}
// Suscribe to changes
DownloadService.runningRelay
@ -83,12 +80,12 @@ class DownloadController : NucleusController<DownloadPresenter>() {
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
adapter = null
super.onDestroyView(view)
}
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.
*/
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
*/
private fun setInformationView() {
val emptyView = view?.empty_view ?: return
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)
} else {
emptyView.hide()
empty_view?.hide()
}
}

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
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.
* @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

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.
*
@ -45,6 +47,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/
override fun bindView(view: View, position: Int) {
(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) {
(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
}
/**
* 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() {
adapter.setItems(emptyList())
adapter.clearSelection()
subscriptions.clear()
unsubscribe()
}
override fun onDetachedFromWindow() {
fun unsubscribe() {
subscriptions.clear()
super.onDetachedFromWindow()
}
/**

View File

@ -14,8 +14,6 @@ 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.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v4.view.pageSelections
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.SecondaryDrawerController
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.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
@ -41,7 +40,7 @@ import exh.metadata.models.SearchableGalleryMetadata
import io.realm.Realm
import io.realm.RealmResults
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.library_controller.view.*
import kotlinx.android.synthetic.main.library_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -108,14 +107,8 @@ class LibraryController(
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
/**
@ -141,6 +134,7 @@ class LibraryController(
init {
setHasOptionsMenu(true)
retainViewMode = RetainViewMode.RETAIN_DETACH
}
override fun getTitle(): String? {
@ -165,13 +159,12 @@ class LibraryController(
}
// <-- EH
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = LibraryAdapter(this)
with(view) {
view_pager.adapter = adapter
view_pager.pageSelections().skip(1).subscribeUntilDestroy {
library_pager.adapter = adapter
library_pager.pageSelections().skip(1).subscribeUntilDestroy {
preferences.lastUsedCategory().set(it)
activeCategory = it
}
@ -186,22 +179,22 @@ class LibraryController(
createActionModeIfNeeded()
}
}
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager)
activity?.tabs?.setupWithViewPager(library_pager)
presenter.subscribeLibrary()
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter?.onDestroy()
adapter = null
actionMode = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
super.onDestroyView(view)
// --> EH
//Clean up realm
@ -263,14 +256,14 @@ class LibraryController(
// Show empty view if needed
if (mangaMap.isNotEmpty()) {
view.empty_view.hide()
empty_view.hide()
} 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.
val activeCat = if (adapter.categories.isNotEmpty())
view.view_pager.currentItem
library_pager.currentItem
else
activeCategory
@ -278,14 +271,14 @@ class LibraryController(
adapter.categories = categories
// Restore active category.
view.view_pager.setCurrentItem(activeCat, false)
library_pager.setCurrentItem(activeCat, false)
tabsVisibilityRelay.call(categories.size > 1)
// Delay the scroll position to allow the view to be properly measured.
view.post {
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
*/
private fun reattachAdapter() {
val pager = view?.view_pager ?: return
val adapter = adapter ?: return
val position = pager.currentItem
val position = library_pager.currentItem
adapter.recycle = false
pager.adapter = adapter
pager.currentItem = position
library_pager.adapter = adapter
library_pager.currentItem = position
adapter.recycle = true
}
@ -361,7 +353,7 @@ class LibraryController(
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
if (!query.isNullOrEmpty()) {
if (!query.isEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
@ -395,15 +387,13 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_filter -> {
navView?.let { drawer?.openDrawer(Gravity.END) }
navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
}
R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) }
}
R.id.action_edit_categories -> {
router.pushController(RouterTransaction.with(CategoryController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(CategoryController().withFadeTransaction())
}
else -> return super.onOptionsItemSelected(item)
}
@ -459,9 +449,7 @@ class LibraryController(
// Notify the presenter a manga is being opened.
presenter.onOpenManga()
router.pushController(RouterTransaction.with(MangaController(manga))
.pushChangeHandler(FadeChangeHandler(false))
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga).withFadeTransaction())
}
/**
@ -496,11 +484,11 @@ class LibraryController(
.toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
.showDialog(router, null)
.showDialog(router)
}
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>) {
@ -515,8 +503,6 @@ class LibraryController(
/**
* Changes the cover for the selected manga.
*
* @param mangas a list of selected manga.
*/
private fun changeSelectedCover() {
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.kanade.tachiyomi.data.glide.GlideApp
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.
@ -30,30 +30,28 @@ class LibraryGridHolder(
*/
override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
view.title.text = item.manga.title
title.text = item.manga.title
// 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
text = item.manga.unread.toString()
}
// Update the download count and its visibility.
with(view.download_text) {
with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = item.downloadCount.toString()
}
//set local visibility if its local manga
with(view.local_text) {
visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
// Update the cover.
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
GlideApp.with(view.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(view.thumbnail)
.into(thumbnail)
}
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View
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.
@ -14,7 +14,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
abstract class LibraryHolder(
view: View,
adapter: FlexibleAdapter<*>
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
/**
* 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.kanade.tachiyomi.data.glide.GlideApp
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.
@ -30,38 +30,36 @@ class LibraryListHolder(
*/
override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
itemView.title.text = item.manga.title
title.text = item.manga.title
// 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
text = item.manga.unread.toString()
}
// Update the download count and its visibility.
with(itemView.download_text) {
with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = "${item.downloadCount}"
}
//show local text badge if local manga
with(itemView.local_text) {
visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
// Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener {
thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
}
// Update the cover.
GlideApp.with(itemView.context).clear(itemView.thumbnail)
GlideApp.with(itemView.context).clear(thumbnail)
GlideApp.with(itemView.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.circleCrop()
.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.view.ViewGroup
import com.bluelinelabs.conductor.*
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController
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_recent_updates -> setRoot(RecentChaptersController(), 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
R.id.nav_drawer_batch_add -> setRoot(BatchAddController(), id)
// <-- EHX
R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(DownloadController().withFadeTransaction())
}
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)
@ -225,10 +218,7 @@ class MainActivity : BaseActivity() {
}
private fun setRoot(controller: Controller, id: Int) {
router.setRoot(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler())
.tag(id.toString()))
router.setRoot(controller.withFadeTransaction().tag(id.toString()))
}
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {

View File

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

View File

@ -2,41 +2,41 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.view.View
import android.widget.PopupMenu
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.chapters_item.view.*
import kotlinx.android.synthetic.main.chapters_item.*
import java.util.*
class ChapterHolder(
private val view: View,
private val adapter: ChaptersAdapter
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
init {
// 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
// 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
chapter_title.text = when (manga.displayMode) {
Manga.DISPLAY_NUMBER -> {
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
}
// 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
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
@ -53,14 +53,14 @@ class ChapterHolder(
chapter_scanlator.text = chapter.scanlator
//allow longer titles if there is no scanlator (most sources)
if (chapter_scanlator.text.isNullOrBlank()) {
chapter_title.setMaxLines(2)
chapter_title.maxLines = 2
chapter_scanlator.gone()
} else {
chapter_title.setMaxLines(1)
chapter_title.maxLines = 1
}
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 {
""
}
@ -68,7 +68,7 @@ class ChapterHolder(
notifyStatus(item.status)
}
fun notifyStatus(status: Int) = with(view.download_text) {
fun notifyStatus(status: Int) = with(download_text) {
when (status) {
Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -4,7 +4,6 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
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.snack
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
class ChaptersController : NucleusController<ChaptersPresenter>(),
@ -69,18 +68,17 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
return inflater.inflate(R.layout.chapters_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Init RecyclerView and adapter
adapter = ChaptersAdapter(this, view.context)
with(view) {
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(context)
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter?.fastScroller = view.fast_scroller
adapter?.fastScroller = fast_scroller
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
@ -100,26 +98,25 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
openChapter(item.chapter)
}
} else {
context.toast(R.string.no_next_chapter)
}
view.context.toast(R.string.no_next_chapter)
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
actionMode = null
super.onDestroyView(view)
}
override fun onActivityResumed(activity: Activity) {
val view = view ?: return
if (view == null) return
// Check if animation view is visible
if (view.reveal_view.visibility == View.VISIBLE) {
if (reveal_view.visibility == View.VISIBLE) {
// Show the unReveal effect
val coordinates = view.fab.getCoordinates()
view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
val coordinates = fab.getCoordinates()
reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
}
super.onActivityResumed(activity)
}
@ -213,16 +210,16 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
fun fetchChaptersFromSource() {
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
presenter.fetchChaptersFromSource()
}
fun onFetchChaptersDone() {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
}
fun onFetchChaptersError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
activity?.toast(error.message)
}
@ -231,7 +228,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
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) {
@ -365,7 +362,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
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) {
presenter.addToLibrary()
}

View File

@ -39,7 +39,7 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import jp.wasabeef.glide.transformations.CropSquareTransformation
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 java.text.DecimalFormat
@ -71,10 +71,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
return inflater.inflate(R.layout.manga_info_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
@ -82,8 +81,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.manga_info, menu)
}
@ -124,7 +121,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
*/
private fun setMangaInfo(manga: Manga, source: Source?) {
val view = view ?: return
with(view) {
// Update artist TextView.
manga_artist.text = manga.artist
@ -155,14 +152,14 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(context)
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(manga_cover)
if (backdrop != null) {
GlideApp.with(context)
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
@ -170,7 +167,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
}
}
}
}
/**
* Update chapter count TextView.
@ -178,7 +174,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param count number of chapters.
*/
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) {
// Set the Favorite drawable to the correct one.
// 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
else
R.drawable.ic_bookmark_border_white_24dp)
@ -279,7 +275,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param value whether it should be refreshing or not.
*/
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
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
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.manga.MangaController
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>(),
TrackAdapter.OnRowClickListener,
@ -35,8 +34,8 @@ class TrackController : NucleusController<TrackPresenter>(),
return inflater.inflate(R.layout.track_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = TrackAdapter(this)
with(view) {
@ -48,14 +47,14 @@ class TrackController : NucleusController<TrackPresenter>(),
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
super.onDestroyView(view)
}
fun onNextTrackings(trackings: List<TrackItem>) {
val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings
view?.swipe_refresh?.isEnabled = atLeastOneLink
swipe_refresh?.isEnabled = atLeastOneLink
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
}
@ -73,11 +72,11 @@ class TrackController : NucleusController<TrackPresenter>(),
}
fun onRefreshDone() {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
}
fun onRefreshError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
activity?.toast(error.message)
}
@ -109,17 +108,17 @@ class TrackController : NucleusController<TrackPresenter>(),
override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
override fun setScore(item: TrackItem, score: Int) {
presenter.setScore(item, score)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
presenter.setLastChapterRead(item, chaptersRead)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
private companion object {

View File

@ -1,29 +1,29 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import android.view.View
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 {
val listener = adapter.rowClickListener
view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
}
@SuppressLint("SetTextI18n")
@Suppress("DEPRECATION")
fun bind(item: TrackItem) = with(itemView) {
fun bind(item: TrackItem) {
val track = item.track
track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor())
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.text = track.title
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_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
} 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_chapters.text = ""
track_score.text = ""

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@ -11,10 +10,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
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.viewer.base.PageDecodeErrorLayout
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.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder.
*/
class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) :
RecyclerView.ViewHolder(view) {
BaseViewHolder(view) {
/**
* Page of a chapter.
@ -54,7 +54,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
private var decodeErrorLayout: View? = null
init {
with(view.image_view) {
with(image_view) {
setMaxTileSize(readerActivity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
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)
view.setOnTouchListener(adapter.touchListener)
view.retry_button.setOnTouchListener { _, event ->
retry_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
readerActivity.presenter.retryPage(page)
}
@ -111,9 +111,9 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
(view as ViewGroup).removeView(it)
decodeErrorLayout = null
}
view.image_view.recycle()
view.image_view.visibility = View.GONE
view.progress_container.visibility = View.VISIBLE
image_view.recycle()
image_view.visibility = View.GONE
progress_container.visibility = View.VISIBLE
}
/**
@ -150,7 +150,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
view.progress_text.text = if (progress > 0) {
progress_text.text = if (progress > 0) {
view.context.getString(R.string.download_progress, progress)
} else {
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.
*/
private fun onImageDecoded() {
view.progress_container.visibility = View.GONE
progress_container.visibility = View.GONE
}
/**
* Called when the image fails to decode.
*/
private fun onImageDecodeError() {
view.progress_container.visibility = View.GONE
progress_container.visibility = View.GONE
val page = page ?: 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.widget.PopupMenu
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
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.setVectorCompat
import kotlinx.android.synthetic.main.recent_chapters_item.view.*
import kotlinx.android.synthetic.main.recent_chapters_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.
*/
class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) :
FlexibleViewHolder(view, adapter) {
BaseFlexibleViewHolder(view, adapter) {
/**
* 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
// correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
view.manga_cover.setOnClickListener {
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
manga_cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition)
}
}
@ -58,31 +58,31 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
this.item = item
// Set chapter title
view.chapter_title.text = item.chapter.name
chapter_title.text = item.chapter.name
// 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.
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
GlideApp.with(itemView.context).clear(itemView.manga_cover)
GlideApp.with(itemView.context).clear(manga_cover)
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.circleCrop()
.into(itemView.manga_cover)
.into(manga_cover)
}
// Check if chapter is read and set correct color
if (item.chapter.read) {
view.chapter_title.setTextColor(readColor)
view.manga_title.setTextColor(readColor)
chapter_title.setTextColor(readColor)
manga_title.setTextColor(readColor)
} else {
view.chapter_title.setTextColor(unreadColor)
view.manga_title.setTextColor(unreadColor)
chapter_title.setTextColor(unreadColor)
manga_title.setTextColor(unreadColor)
}
// Set chapter status
@ -94,7 +94,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
*
* @param status download status
*/
fun notifyStatus(status: Int) = with(view.download_text) {
fun notifyStatus(status: Int) = with(download_text) {
when (status) {
Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
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.v7.widget.scrollStateChanges
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.NucleusController
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.reader.ReaderActivity
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
/**
@ -65,16 +63,14 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
/**
* Called when view is created
* @param view created view
* @param savedViewState status of saved sate
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Init RecyclerView and adapter
val layoutManager = LinearLayoutManager(context)
val layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter
@ -85,22 +81,21 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
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 {
if (!LibraryUpdateService.isRunning(context)) {
LibraryUpdateService.start(context)
context.toast(R.string.action_update_library)
if (!LibraryUpdateService.isRunning(view.context)) {
LibraryUpdateService.start(view.context)
view.context.toast(R.string.action_update_library)
}
// It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
actionMode = null
super.onDestroyView(view)
}
/**
@ -180,11 +175,10 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
}
override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) {
emptyView.hide()
empty_view?.hide()
} 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.
*/
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) {
router.pushController(RouterTransaction.with(MangaController(chapter.manga))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(chapter.manga).withFadeTransaction())
}
/**

View File

@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.ui.recently_read
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
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.reader.ReaderActivity
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.
@ -51,23 +49,20 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
* Called when view is created
*
* @param view created view
* @param savedViewState saved state of the view
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Initialize adapter
recycler.layoutManager = LinearLayoutManager(context)
recycler.layoutManager = LinearLayoutManager(view.context)
adapter = RecentlyReadAdapter(this@RecentlyReadController)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
super.onDestroyView(view)
}
/**
@ -80,11 +75,10 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
}
override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) {
emptyView.hide()
empty_view.hide()
} 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) {
val manga = adapter?.getItem(position)?.mch?.manga ?: return
router.pushController(RouterTransaction.with(MangaController(manga))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga).withFadeTransaction())
}
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 com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
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.*
/**
@ -21,18 +21,18 @@ import java.util.*
class RecentlyReadHolder(
view: View,
val adapter: RecentlyReadAdapter
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
init {
itemView.remove.setOnClickListener {
remove.setOnClickListener {
adapter.removeClickListener.onRemoveClick(adapterPosition)
}
itemView.resume.setOnClickListener {
resume.setOnClickListener {
adapter.resumeClickListener.onResumeClick(adapterPosition)
}
itemView.cover.setOnClickListener {
cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition)
}
}
@ -47,24 +47,24 @@ class RecentlyReadHolder(
val (manga, chapter, history) = item
// Set manga title
itemView.manga_title.text = manga.title
manga_title.text = manga.title
// Set source + chapter title
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)
// 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
GlideApp.with(itemView.context).clear(itemView.cover)
GlideApp.with(itemView.context).clear(cover)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(itemView.cover)
.into(cover)
}
}

View File

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

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.setting
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.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.getResourceColor
class SettingsMainController : SettingsController() {
@ -69,8 +68,6 @@ class SettingsMainController : SettingsController() {
}
private fun navigateTo(controller: SettingsController) {
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(controller.withFadeTransaction())
}
}

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup
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.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:layout_width="match_parent"
android:layout_height="match_parent">

View File

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

View File

@ -1,5 +1,5 @@
<?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"
android:id="@+id/nav_view2"
android:layout_width="wrap_content"

View File

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

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
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_height="match_parent"
android:background="?android:attr/colorBackground" />
android:layout_height="match_parent" />

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<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="">
<changelogtext>Added a download cache for faster navigation.</changelogtext>

View File

@ -201,7 +201,7 @@
<string name="dialog_restoring_backup">Backup wird wiederhergestellt
%1$s zur Bibliothek hinzugefügt</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>
<string name="backup_created">Backup erstellt</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_item" type="id"/>
<item name="reader_pager" type="id"/>
</resources>