Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	README.md
#	app/build.gradle
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
#	app/src/main/res/menu/library.xml
#	app/src/main/res/values/strings.xml
#	app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt
This commit is contained in:
NerdNumber9
2018-01-29 12:16:32 -05:00
134 changed files with 2903 additions and 1215 deletions

View File

@@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.os.Build
import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.util.dpToPx
import io.github.mthli.slice.Slice
interface SlicedHolder {
val slice: Slice
val adapter: FlexibleAdapter<IFlexible<*>>
val viewToSlice: View
fun setCardEdges(item: ISectionable<*, *>) {
// Position of this item in its header. Defaults to 0 when header is null.
var position = 0
// Number of items in the header of this item. Defaults to 1 when header is null.
var count = 1
if (item.header != null) {
val sectionItems = adapter.getSectionItems(item.header)
position = sectionItems.indexOf(item)
count = sectionItems.size
}
when {
// Only one item in the card
count == 1 -> applySlice(2f, false, false, true, true)
// First item of the card
position == 0 -> applySlice(2f, false, true, true, false)
// Last item of the card
position == count - 1 -> applySlice(2f, true, false, false, true)
// Middle item
else -> applySlice(0f, false, false, false, false)
}
}
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
topShadow: Boolean, bottomShadow: Boolean) {
val margin = margin
slice.setRadius(radius)
slice.showLeftTopRect(topRect)
slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
if (viewToSlice.layoutParams is ViewGroup.MarginLayoutParams) {
val p = viewToSlice.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)
}
}
val margin
get() = 8.dpToPx
}

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.base.presenter
import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery
import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() {
@@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() {
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
/**
* A deliverable that only emits to the view if attached, otherwise the event is ignored.
*/
class DeliverWithView<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable
.materialize()
.filter { notification -> !notification.isOnCompleted }
.flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) }
}
}
}
}

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
@@ -15,6 +16,7 @@ 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.base.controller.requestPermissionsSafe
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
@@ -46,7 +48,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
/**
* Adapter containing sources.
*/
private var adapter : CatalogueAdapter? = null
private var adapter: CatalogueAdapter? = null
/**
* Called when controller is initialized.
@@ -99,6 +101,8 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
}
override fun onDestroyView(view: View) {
@@ -185,10 +189,11 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController(CatalogueSearchController(query).withFadeTransaction())
}
.subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) }
}
fun performGlobalSearch(query: String){
router.pushController(CatalogueSearchController(query).withFadeTransaction())
}
/**

View File

@@ -12,23 +12,23 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(ATTRS)
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
divider = a.getDrawable(0)
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder &&
val holder = parent.getChildViewHolder(child)
if (holder is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
val left = parent.paddingLeft + holder.margin
val right = parent.paddingRight + holder.margin
divider.setBounds(left, top, right, bottom)
divider.draw(c)
@@ -41,7 +41,4 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider)
}
}

View File

@@ -1,24 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.os.Build
import android.view.View
import android.view.ViewGroup
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.ui.base.holder.SlicedHolder
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.*
class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) {
class SourceHolder(view: View, override val adapter: CatalogueAdapter) :
BaseFlexibleViewHolder(view, adapter),
SlicedHolder {
private val slice = Slice(card).apply {
override val slice = Slice(card).apply {
setColor(adapter.cardBackground)
}
override val viewToSlice: View
get() = card
init {
source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition)
@@ -38,7 +41,7 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold
// Set circle letter image.
itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(), false))
}
// If source is login, show only login option
@@ -47,59 +50,11 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold
source_latest.gone()
} else {
source_browse.setText(R.string.browse)
source_latest.visible()
if (source.supportsLatest) {
source_latest.visible()
} else {
source_latest.gone()
}
}
}
private fun setCardEdges(item: SourceItem) {
// Position of this item in its header. Defaults to 0 when header is null.
var position = 0
// Number of items in the header of this item. Defaults to 1 when header is null.
var count = 1
if (item.header != null) {
val sectionItems = mAdapter.getSectionItems(item.header)
position = sectionItems.indexOf(item)
count = sectionItems.size
}
when {
// Only one item in the card
count == 1 -> applySlice(2f, false, false, true, true)
// First item of the card
position == 0 -> applySlice(2f, false, true, true, false)
// Last item of the card
position == count - 1 -> applySlice(2f, true, false, false, true)
// Middle item
else -> applySlice(0f, false, false, false, false)
}
}
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
topShadow: Boolean, bottomShadow: Boolean) {
slice.setRadius(radius)
slice.showLeftTopRect(topRect)
slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = card
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)
}
}
companion object {
val margin = 8.dpToPx
}
}

View File

@@ -24,7 +24,6 @@ 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
@@ -75,11 +74,6 @@ open class BrowseCatalogueController(bundle: Bundle) :
*/
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.
*/
@@ -138,17 +132,15 @@ open class BrowseCatalogueController(bundle: Bundle) :
// 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)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
drawer.closeDrawer(Gravity.END)
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
@@ -162,8 +154,6 @@ open class BrowseCatalogueController(bundle: Bundle) :
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
@@ -171,13 +161,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
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
val oldRecycler = catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
catalogue_view?.removeView(oldRecycler)
}
catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
@@ -476,6 +466,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
}.show()
@@ -498,6 +489,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
activity?.toast(activity?.getString(R.string.manga_added_library))
}
}

View File

@@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view)
title.text = context?.getString(R.string.source_search_options)
search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() }
}

View File

@@ -13,6 +13,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
init {
isExpanded = false
}
override fun getLayoutRes(): Int {
return R.layout.navigation_view_group
}
@@ -32,6 +36,9 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
R.drawable.ic_expand_more_white_24dp
else
R.drawable.ic_chevron_right_white_24dp)
holder.itemView.setOnClickListener(holder)
}
override fun equals(other: Any?): Boolean {
@@ -44,6 +51,7 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
return filter.hashCode()
}
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
val title: TextView = itemView.findViewById(R.id.title)
@@ -52,5 +60,6 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
override fun shouldNotifyParentOnClick(): Boolean {
return true
}
}
}

View File

@@ -10,6 +10,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
init {
isExpanded = false
}
override fun getLayoutRes(): Int {
return R.layout.navigation_view_group
}
@@ -29,6 +33,9 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou
R.drawable.ic_expand_more_white_24dp
else
R.drawable.ic_chevron_right_white_24dp)
holder.itemView.setOnClickListener(holder)
}
override fun equals(other: Any?): Boolean {

View File

@@ -33,9 +33,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
val i = filter.values.indexOf(name)
fun getIcon() = when (filter.state) {
Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_down_black_32dp, null)
Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_down_32dp, null)
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_up_black_32dp, null)
Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_up_32dp, null)
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp)
}

View File

@@ -22,6 +22,7 @@ class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
*/
interface OnMangaClickListener {
fun onMangaClick(manga: Manga)
fun onMangaLongClick(manga: Manga)
}
}

View File

@@ -19,10 +19,19 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
adapter.mangaClickListener.onMangaClick(item.manga)
}
}
itemView.setOnLongClickListener {
val item = adapter.getItem(adapterPosition)
if (item != null) {
adapter.mangaClickListener.onMangaLongClick(item.manga)
}
true
}
}
fun bind(manga: Manga) {
tvTitle.text = manga.title
// Set alpha of thumbnail.
itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
setImage(manga)
}

View File

@@ -18,14 +18,14 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller.*
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/
class CatalogueSearchController(private val initialQuery: String? = null) :
open class CatalogueSearchController(protected val initialQuery: String? = null) :
NucleusController<CatalogueSearchPresenter>(),
CatalogueSearchCardAdapter.OnMangaClickListener {
/**
* Adapter containing search results grouped by lang.
*/
private var adapter: CatalogueSearchAdapter? = null
protected var adapter: CatalogueSearchAdapter? = null
/**
* Called when controller is initialized.
@@ -73,6 +73,16 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
router.pushController(MangaController(manga, true).withFadeTransaction())
}
/**
* Called when manga in global search is long clicked.
*
* @param manga clicked item containing manga information.
*/
override fun onMangaLongClick(manga: Manga) {
// Delegate to single click by default.
onMangaClick(manga)
}
/**
* Adds items to the options menu.
*

View File

@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
* @param db manages the database calls.
* @param preferencesHelper manages the preference calls.
*/
class CatalogueSearchPresenter(
open class CatalogueSearchPresenter(
val initialQuery: String? = "",
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
@@ -86,7 +86,7 @@ class CatalogueSearchPresenter(
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
protected open fun getEnabledSources(): List<CatalogueSource> {
val languages = preferencesHelper.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()

View File

@@ -26,7 +26,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
CategoryAdapter.OnItemReleaseListener,
CategoryCreateDialog.Listener,
CategoryRenameDialog.Listener,
UndoHelper.OnUndoListener {
UndoHelper.OnActionListener {
/**
* Object used to show ActionMode toolbar.
@@ -107,9 +107,14 @@ class CategoryController : NucleusController<CategoryPresenter>(),
fun setCategories(categories: List<CategoryItem>) {
actionMode?.finish()
adapter?.updateDataSet(categories)
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
if (categories.isNotEmpty()) {
empty_view.hide()
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
}
} else {
empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category)
}
}
@@ -163,7 +168,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this)
undoHelper?.start(adapter.selectedPositions, view!!,
R.string.snack_categories_deleted, R.string.action_undo, 3000)
R.string.snack_categories_deleted, R.string.action_undo, 3000)
mode.finish()
}
@@ -263,7 +268,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*
* @param action The action performed.
*/
override fun onActionCanceled(action: Int) {
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
adapter?.restoreDeletedItems()
undoHelper = null
}

View File

@@ -21,10 +21,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter
import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.manga.track.TrackController
@@ -34,6 +32,7 @@ import kotlinx.android.synthetic.main.manga_controller.*
import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
class MangaController : RxController, TabbedController {
@@ -63,6 +62,8 @@ class MangaController : RxController, TabbedController {
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
@@ -188,4 +189,5 @@ class MangaController : RxController, TabbedController {
.apply { isAccessible = true }
}
}

View File

@@ -36,7 +36,7 @@ class ChapterHolder(
}
// Set the correct drawable for dropdown and update the tint to match theme.
chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set correct text color
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)

View File

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.support.design.widget.Snackbar
@@ -36,6 +37,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
SetDisplayModeDialog.Listener,
SetSortingDialog.Listener,
DownloadChaptersDialog.Listener,
DownloadCustomChaptersDialog.Listener,
DeleteChaptersDialog.Listener {
/**
@@ -61,7 +63,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
override fun createPresenter(): ChaptersPresenter {
val ctrl = parentController as MangaController
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@@ -209,7 +211,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
}
fun fetchChaptersFromSource() {
private fun fetchChaptersFromSource() {
swipe_refresh?.isRefreshing = true
presenter.fetchChaptersFromSource()
}
@@ -271,18 +273,18 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
actionMode?.invalidate()
}
fun getSelectedChapters(): List<ChapterItem> {
private fun getSelectedChapters(): List<ChapterItem> {
val adapter = adapter ?: return emptyList()
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
}
fun createActionModeIfNeeded() {
private fun createActionModeIfNeeded() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
}
fun destroyActionModeIfNeeded() {
private fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
@@ -292,6 +294,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
return true
}
@SuppressLint("StringFormatInvalid")
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = adapter?.selectedItemCount ?: 0
if (count == 0) {
@@ -339,25 +342,25 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
// SELECTION MODE ACTIONS
fun selectAll() {
private fun selectAll() {
val adapter = adapter ?: return
adapter.selectAll()
selectedItems.addAll(adapter.items)
actionMode?.invalidate()
}
fun markAsRead(chapters: List<ChapterItem>) {
private fun markAsRead(chapters: List<ChapterItem>) {
presenter.markChaptersRead(chapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters)
}
}
fun markAsUnread(chapters: List<ChapterItem>) {
private fun markAsUnread(chapters: List<ChapterItem>) {
presenter.markChaptersRead(chapters, false)
}
fun downloadChapters(chapters: List<ChapterItem>) {
private fun downloadChapters(chapters: List<ChapterItem>) {
val view = view
destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
@@ -370,6 +373,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
}
private fun showDeleteChaptersConfirmationDialog() {
DeleteChaptersDialog(this).showDialog(router)
}
@@ -378,7 +382,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
deleteChapters(getSelectedChapters())
}
fun markPreviousAsRead(chapter: ChapterItem) {
private fun markPreviousAsRead(chapter: ChapterItem) {
val adapter = adapter ?: return
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
val chapterPos = chapters.indexOf(chapter)
@@ -387,7 +391,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
}
fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
destroyActionModeIfNeeded()
presenter.bookmarkChapters(chapters, bookmarked)
}
@@ -410,7 +414,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
Timber.e(error)
}
fun dismissDeletingDialog() {
private fun dismissDeletingDialog() {
router.popControllerWithTag(DeletingChaptersDialog.TAG)
}
@@ -439,29 +443,44 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
DownloadChaptersDialog(this).showDialog(router)
}
override fun downloadChapters(choice: Int) {
fun getUnreadChaptersSorted() = presenter.chapters
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
.distinctBy { it.name }
.sortedByDescending { it.source_order }
// i = 0: Download 1
// i = 1: Download 5
// i = 2: Download 10
// i = 3: Download unread
// i = 4: Download all
val chaptersToDownload = when (choice) {
0 -> getUnreadChaptersSorted().take(1)
1 -> getUnreadChaptersSorted().take(5)
2 -> getUnreadChaptersSorted().take(10)
3 -> presenter.chapters.filter { !it.read }
4 -> presenter.chapters
else -> emptyList()
}
private fun getUnreadChaptersSorted() = presenter.chapters
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
.distinctBy { it.name }
.sortedByDescending { it.source_order }
override fun downloadCustomChapters(amount: Int) {
val chaptersToDownload = getUnreadChaptersSorted().take(amount)
if (chaptersToDownload.isNotEmpty()) {
downloadChapters(chaptersToDownload)
}
}
private fun showCustomDownloadDialog() {
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
}
override fun downloadChapters(choice: Int) {
// i = 0: Download 1
// i = 1: Download 5
// i = 2: Download 10
// i = 3: Download x
// i = 4: Download unread
// i = 5: Download all
val chaptersToDownload = when (choice) {
0 -> getUnreadChaptersSorted().take(1)
1 -> getUnreadChaptersSorted().take(5)
2 -> getUnreadChaptersSorted().take(10)
3 -> {
showCustomDownloadDialog()
return
}
4 -> presenter.chapters.filter { !it.read }
5 -> presenter.chapters
else -> emptyList()
}
if (chaptersToDownload.isNotEmpty()) {
downloadChapters(chaptersToDownload)
}
}
}

View File

@@ -20,6 +20,7 @@ import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
/**
* Presenter of [ChaptersController].
@@ -28,6 +29,7 @@ class ChaptersPresenter(
val manga: Manga,
val source: Source,
private val chapterCountRelay: BehaviorRelay<Float>,
private val lastUpdateRelay: BehaviorRelay<Date>,
private val mangaFavoriteRelay: PublishRelay<Boolean>,
val preferences: PreferencesHelper = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
@@ -91,6 +93,11 @@ class ChaptersPresenter(
// Emit the number of chapters to the info tab.
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
?: 0f)
// Emit the upload date of the most recent chapter
lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
?: 0))
}
.subscribe { chaptersRelay.call(it) })
}

View File

@@ -21,12 +21,12 @@ class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundl
R.string.download_1,
R.string.download_5,
R.string.download_10,
R.string.download_custom,
R.string.download_unread,
R.string.download_all
).map { activity.getString(it) }
return MaterialDialog.Builder(activity)
.title(R.string.manga_download)
.negativeText(android.R.string.cancel)
.items(choices)
.itemsCallback { _, _, position, _ ->

View File

@@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.ui.manga.chapter
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.widget.DialogCustomDownloadView
/**
* Dialog used to let user select amount of chapters to download.
*/
class DownloadCustomChaptersDialog<T> : DialogController
where T : Controller, T : DownloadCustomChaptersDialog.Listener {
/**
* Maximum number of chapters to download in download chooser.
*/
private val maxChapters: Int
/**
* Initialize dialog.
* @param maxChapters maximal number of chapters that user can download.
*/
constructor(target: T, maxChapters: Int) : super(Bundle().apply {
// Add maximum number of chapters to download value to bundle.
putInt(KEY_ITEM_MAX, maxChapters)
}) {
targetController = target
this.maxChapters = maxChapters
}
/**
* Restore dialog.
* @param bundle bundle containing data from state restore.
*/
@Suppress("unused")
constructor(bundle: Bundle) : super(bundle) {
// Get maximum chapters to download from bundle
val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0)
this.maxChapters = maxChapters
}
/**
* Called when dialog is being created.
*/
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
// Initialize view that lets user select number of chapters to download.
val view = DialogCustomDownloadView(activity).apply {
setMinMax(0, maxChapters)
}
// Build dialog.
// when positive dialog is pressed call custom listener.
return MaterialDialog.Builder(activity)
.title(R.string.custom_download)
.customView(view, true)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { _, _ ->
(targetController as? Listener)?.downloadCustomChapters(view.amount)
}
.build()
}
interface Listener {
fun downloadCustomChapters(amount: Int)
}
private companion object {
// Key to retrieve max chapters from bundle on process death.
const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters"
}
}

View File

@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.info
import android.app.Dialog
import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
@@ -13,6 +16,7 @@ import android.support.v4.content.pm.ShortcutInfoCompat
import android.support.v4.content.pm.ShortcutManagerCompat
import android.support.v4.graphics.drawable.IconCompat
import android.view.*
import android.widget.Toast
import com.afollestad.materialdialogs.MaterialDialog
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@@ -20,6 +24,7 @@ import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.jakewharton.rxbinding.support.v4.widget.refreshes
import com.jakewharton.rxbinding.view.clicks
import com.jakewharton.rxbinding.view.longClicks
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -31,17 +36,22 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.truncateCenter
import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.manga_info_controller.*
import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
import java.text.DecimalFormat
import java.util.*
/**
* Fragment that shows manga information.
@@ -64,7 +74,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
override fun createPresenter(): MangaInfoPresenter {
val ctrl = parentController as MangaController
return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@@ -79,6 +89,41 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
manga_full_title.longClicks().subscribeUntilDestroy {
copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString())
}
manga_full_title.clicks().subscribeUntilDestroy {
performGlobalSearch(manga_full_title.text.toString())
}
manga_artist.longClicks().subscribeUntilDestroy {
copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString())
}
manga_artist.clicks().subscribeUntilDestroy {
performGlobalSearch(manga_artist.text.toString())
}
manga_author.longClicks().subscribeUntilDestroy {
copyToClipboard(manga_author.text.toString(), manga_author.text.toString())
}
manga_author.clicks().subscribeUntilDestroy {
performGlobalSearch(manga_author.text.toString())
}
manga_summary.longClicks().subscribeUntilDestroy {
copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString())
}
manga_genres_tags.setOnTagClickListener { tag -> performGlobalSearch(tag) }
manga_cover.longClicks().subscribeUntilDestroy {
copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -107,6 +152,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
if (manga.initialized) {
// Update view.
setMangaInfo(manga, source)
} else {
// Initialize manga.
fetchMangaFromSource()
@@ -122,19 +168,45 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
private fun setMangaInfo(manga: Manga, source: Source?) {
val view = view ?: return
// Update artist TextView.
manga_artist.text = manga.artist
// Update author TextView.
manga_author.text = manga.author
// If manga source is known update source TextView.
if (source != null) {
manga_source.text = source.toString()
//update full title TextView.
manga_full_title.text = if (manga.title.isBlank()) {
view.context.getString(R.string.unknown)
} else {
manga.title
}
// Update genres TextView.
manga_genres.text = manga.genre
// Update artist TextView.
manga_artist.text = if (manga.artist.isNullOrBlank()) {
view.context.getString(R.string.unknown)
} else {
manga.artist
}
// Update author TextView.
manga_author.text = if (manga.author.isNullOrBlank()) {
view.context.getString(R.string.unknown)
} else {
manga.author
}
// If manga source is known update source TextView.
manga_source.text = if (source == null) {
view.context.getString(R.string.unknown)
} else {
source.toString()
}
// Update genres list
if (manga.genre.isNullOrBlank().not()) {
manga_genres_tags.setTags(manga.genre?.split(", "))
}
// Update description TextView.
manga_summary.text = if (manga.description.isNullOrBlank()) {
view.context.getString(R.string.unknown)
} else {
manga.description
}
// Update status TextView.
manga_status.setText(when (manga.status) {
@@ -144,9 +216,6 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
else -> R.string.unknown
})
// Update description TextView.
manga_summary.text = manga.description
// Set the favorite drawable to the correct one.
setFavoriteDrawable(manga.favorite)
@@ -168,13 +237,26 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
}
}
override fun onDestroyView(view: View) {
manga_genres_tags.setOnTagClickListener(null)
super.onDestroyView(view)
}
/**
* Update chapter count TextView.
*
* @param count number of chapters.
*/
fun setChapterCount(count: Float) {
manga_chapters?.text = DecimalFormat("#.#").format(count)
if (count > 0f) {
manga_chapters?.text = DecimalFormat("#.#").format(count)
} else {
manga_chapters?.text = resources?.getString(R.string.unknown)
}
}
fun setLastUpdateDate(date: Date) {
manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
}
/**
@@ -242,7 +324,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
fab_favorite?.setImageResource(if (isFavorite)
R.drawable.ic_bookmark_white_24dp
else
R.drawable.ic_bookmark_border_white_24dp)
R.drawable.ic_add_to_library_24dp)
}
/**
@@ -301,6 +383,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
.showDialog(router)
}
}
activity?.toast(activity?.getString(R.string.manga_added_library))
} else {
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
@@ -377,6 +462,35 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
})
}
/**
* Copies a string to clipboard
*
* @param label Label to show to the user describing the content
* @param content the actual text to copy to the board
*/
private fun copyToClipboard(label: String, content: String) {
if (content.isBlank()) return
val activity = activity ?: return
val view = view ?: return
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(label, content)
activity.toast(view.context.getString(R.string.copied_to_clipboard, content.truncateCenter(20)),
Toast.LENGTH_SHORT)
}
/**
* Perform a global search using the provided query.
*
* @param query the search query to pass to the search controller
*/
fun performGlobalSearch(query: String) {
val router = parentController?.router ?: return
router.pushController(CatalogueSearchController(query).withFadeTransaction())
}
/**
* Create shortcut using ShortcutManager.
*

View File

@@ -18,6 +18,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
/**
* Presenter of MangaInfoFragment.
@@ -28,6 +29,7 @@ class MangaInfoPresenter(
val manga: Manga,
val source: Source,
private val chapterCountRelay: BehaviorRelay<Float>,
private val lastUpdateRelay: BehaviorRelay<Date>,
private val mangaFavoriteRelay: PublishRelay<Boolean>,
private val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
@@ -37,7 +39,7 @@ class MangaInfoPresenter(
/**
* Subscription to send the manga to the view.
*/
private var viewMangaSubcription: Subscription? = null
private var viewMangaSubscription: Subscription? = null
/**
* Subscription to update the manga from the source.
@@ -56,14 +58,18 @@ class MangaInfoPresenter(
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
.subscribe { setFavorite(it) }
.apply { add(this) }
//update last update date
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
}
/**
* Sends the active manga to the view.
*/
fun sendMangaToView() {
viewMangaSubcription?.let { remove(it) }
viewMangaSubcription = Observable.just(manga)
viewMangaSubscription?.let { remove(it) }
viewMangaSubscription = Observable.just(manga)
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
}

View File

@@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.ui.migration
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
class MangaAdapter(controller: MigrationController) :
FlexibleAdapter<IFlexible<*>>(null, controller) {
private var items: List<IFlexible<*>>? = null
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
if (this.items !== items) {
this.items = items
super.updateDataSet(items)
}
}
}

View File

@@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.ui.migration
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_list_item.*
class MangaHolder(
private val view: View,
private val adapter: FlexibleAdapter<*>
) : BaseFlexibleViewHolder(view, adapter) {
fun bind(item: MangaItem) {
// Update the title of the manga.
title.text = item.manga.title
// Create thumbnail onclick to simulate long click
thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
}
// Update the cover.
GlideApp.with(itemView.context).clear(thumbnail)
GlideApp.with(itemView.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.circleCrop()
.dontAnimate()
.into(thumbnail)
}
}

View File

@@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.ui.migration
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() {
override fun getLayoutRes(): Int {
return R.layout.catalogue_list_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): MangaHolder {
return MangaHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
holder: MangaHolder,
position: Int,
payloads: List<Any?>?) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (other is MangaItem) {
return manga.id == other.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.id!!.hashCode()
}
}

View File

@@ -0,0 +1,135 @@
package eu.kanade.tachiyomi.ui.migration
import android.app.Dialog
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import kotlinx.android.synthetic.main.migration_controller.*
class MigrationController : NucleusController<MigrationPresenter>(),
FlexibleAdapter.OnItemClickListener,
SourceAdapter.OnSelectClickListener {
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
private var title: String? = null
set(value) {
field = value
setTitle()
}
override fun createPresenter(): MigrationPresenter {
return MigrationPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.migration_controller, container, false)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = FlexibleAdapter(null, this)
migration_recycler.layoutManager = LinearLayoutManager(view.context)
migration_recycler.adapter = adapter
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun getTitle(): String? {
return title
}
override fun handleBack(): Boolean {
return if (presenter.state.selectedSource != null) {
presenter.deselectSource()
true
} else {
super.handleBack()
}
}
fun render(state: ViewState) {
if (state.selectedSource == null) {
title = resources?.getString(R.string.label_migration)
if (adapter !is SourceAdapter) {
adapter = SourceAdapter(this)
migration_recycler.adapter = adapter
}
adapter?.updateDataSet(state.sourcesWithManga)
} else {
title = state.selectedSource.toString()
if (adapter !is MangaAdapter) {
adapter = MangaAdapter(this)
migration_recycler.adapter = adapter
}
adapter?.updateDataSet(state.mangaForSource)
}
}
fun renderIsReplacingManga(state: ViewState) {
if (state.isReplacingManga) {
if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) {
LoadingController().showDialog(router, LOADING_DIALOG_TAG)
}
} else {
router.popControllerWithTag(LOADING_DIALOG_TAG)
}
}
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) ?: return false
if (item is MangaItem) {
val controller = SearchController(item.manga)
controller.targetController = this
router.pushController(controller.withFadeTransaction())
} else if (item is SourceItem) {
presenter.setSelectedSource(item.source)
}
return false
}
override fun onSelectClick(position: Int) {
onItemClick(position)
}
fun migrateManga(prevManga: Manga, manga: Manga) {
presenter.migrateManga(prevManga, manga, replace = true)
}
fun copyManga(prevManga: Manga, manga: Manga) {
presenter.migrateManga(prevManga, manga, replace = false)
}
class LoadingController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog.Builder(activity!!)
.progress(true, 0)
.content(R.string.migrating)
.cancelable(false)
.build()
}
}
companion object {
const val LOADING_DIALOG_TAG = "LoadingDialog"
}
}

View File

@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.R
object MigrationFlags {
private const val CHAPTERS = 0b001
private const val CATEGORIES = 0b010
private const val TRACK = 0b100
private const val CHAPTERS2 = 0x1
private const val CATEGORIES2 = 0x2
private const val TRACK2 = 0x4
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track)
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK)
fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0
}
fun hasCategories(value: Int): Boolean {
return value and CATEGORIES != 0
}
fun hasTracks(value: Int): Boolean {
return value and TRACK != 0
}
fun getEnabledFlagsPositions(value: Int): List<Int> {
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
}
fun getFlagsFromPositions(positions: Array<Int>): Int {
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
}
}

View File

@@ -0,0 +1,151 @@
package eu.kanade.tachiyomi.ui.migration
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.combineLatest
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationPresenter(
private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<MigrationController>() {
var state = ViewState()
private set(value) {
field = value
stateRelay.call(value)
}
private val stateRelay = BehaviorRelay.create(state)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
db.getLibraryMangas()
.asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) }
.combineLatest(stateRelay.map { it.selectedSource }
.distinctUntilChanged(),
{ library, source -> library to source })
.filter { (_, source) -> source != null }
.observeOn(Schedulers.io())
.map { (library, source) -> libraryToMigrationItem(library, source!!.id) }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { state = state.copy(mangaForSource = it) }
.subscribe()
stateRelay
// Render the view when any field other than isReplacingManga changes
.distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga }
.subscribeLatestCache(MigrationController::render)
stateRelay.distinctUntilChanged { state -> state.isReplacingManga }
.subscribeLatestCache(MigrationController::renderIsReplacingManga)
}
fun setSelectedSource(source: Source) {
state = state.copy(selectedSource = source, mangaForSource = emptyList())
}
fun deselectSource() {
state = state.copy(selectedSource = null, mangaForSource = emptyList())
}
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.get(it) else null }
.map { SourceItem(it, header) }
}
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::MangaItem)
}
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
val source = sourceManager.get(manga.source) ?: return
state = state.copy(isReplacingManga = true)
Observable.defer { source.fetchChapterList(manga) }
.onErrorReturn { emptyList() }
.doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) }
.onErrorReturn { emptyList() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnUnsubscribe { state = state.copy(isReplacingManga = false) }
.subscribe()
}
private fun migrateMangaInternal(source: Source, sourceChapters: List<SChapter>,
prevManga: Manga, manga: Manga, replace: Boolean) {
val flags = preferences.migrateFlags().getOrDefault()
val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags)
val migrateTracks = MigrationFlags.hasTracks(flags)
db.inTransaction {
// Update chapters read
if (migrateChapters) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }
.maxBy { it.chapter_number }?.chapter_number
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
chapter.read = true
}
}
db.insertChapters(dbChapters).executeAsBlocking()
}
}
// Update categories
if (migrateCategories) {
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mangaCategories, listOf(manga))
}
// Update track
if (migrateTracks) {
val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) {
track.id = null
track.manga_id = manga.id!!
}
db.insertTracks(tracks).executeAsBlocking()
}
// Update favorite status
if (replace) {
prevManga.favorite = false
db.updateMangaFavorite(prevManga).executeAsBlocking()
}
manga.favorite = true
db.updateMangaFavorite(manga).executeAsBlocking()
}
}
}

View File

@@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.ui.migration
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
import uy.kohesive.injekt.injectLazy
class SearchController(
private var manga: Manga? = null
) : CatalogueSearchController(manga?.title) {
private var newManga: Manga? = null
override fun createPresenter(): CatalogueSearchPresenter {
return SearchPresenter(initialQuery, manga!!)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(::manga.name, manga)
outState.putSerializable(::newManga.name, newManga)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
manga = savedInstanceState.getSerializable(::manga.name) as? Manga
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
}
fun migrateManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return
val newManga = newManga ?: return
router.popController(this)
target.migrateManga(manga, newManga)
}
fun copyManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return
val newManga = newManga ?: return
router.popController(this)
target.copyManga(manga, newManga)
}
override fun onMangaClick(manga: Manga) {
newManga = manga
val dialog = MigrationDialog()
dialog.targetController = this
dialog.showDialog(router)
}
override fun onMangaLongClick(manga: Manga) {
// Call parent's default click listener
super.onMangaClick(manga)
}
class MigrationDialog : DialogController() {
private val preferences: PreferencesHelper by injectLazy()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val prefValue = preferences.migrateFlags().getOrDefault()
val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue)
return MaterialDialog.Builder(activity!!)
.content(R.string.migration_dialog_what_to_include)
.items(MigrationFlags.titles.map { resources?.getString(it) })
.alwaysCallMultiChoiceCallback()
.itemsCallbackMultiChoice(preselected.toTypedArray(), { _, positions, _ ->
// Save current settings for the next time
val newValue = MigrationFlags.getFlagsFromPositions(positions)
preferences.migrateFlags().set(newValue)
true
})
.positiveText(R.string.migrate)
.negativeText(R.string.copy)
.neutralText(android.R.string.cancel)
.onPositive { _, _ ->
(targetController as? SearchController)?.migrateManga()
}
.onNegative { _, _ ->
(targetController as? SearchController)?.copyManga()
}
.build()
}
}
}

View File

@@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
class SearchPresenter(
initialQuery: String? = "",
private val manga: Manga
) : CatalogueSearchPresenter(initialQuery) {
override fun getEnabledSources(): List<CatalogueSource> {
// Filter out the source of the selected manga
return super.getEnabledSources()
.filterNot { it.id == manga.source }
}
}

View File

@@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.migration
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
/**
* Item that contains the selection header.
*/
class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return SelectionHeader.Holder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder,
position: Int, payloads: List<Any?>?) {
// Intentionally empty
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) {
init {
title.text = "Please select a source to migrate from"
}
}
override fun equals(other: Any?): Boolean {
return other is SelectionHeader
}
override fun hashCode(): Int {
return 0
}
}

View File

@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.migration
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.getResourceColor
/**
* Adapter that holds the catalogue cards.
*
* @param controller instance of [MigrationController].
*/
class SourceAdapter(val controller: MigrationController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
private var items: List<IFlexible<*>>? = null
init {
setDisplayHeadersAtStartUp(true)
}
/**
* Listener for browse item clicks.
*/
val selectClickListener: OnSelectClickListener? = controller
/**
* Listener which should be called when user clicks select.
*/
interface OnSelectClickListener {
fun onSelectClick(position: Int)
}
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
if (this.items !== items) {
this.items = items
super.updateDataSet(items)
}
}
}

View File

@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.migration
import android.view.View
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
class SourceHolder(view: View, override val adapter: SourceAdapter) :
BaseFlexibleViewHolder(view, adapter),
SlicedHolder {
override val slice = Slice(card).apply {
setColor(adapter.cardBackground)
}
override val viewToSlice: View
get() = card
init {
source_latest.gone()
source_browse.setText(R.string.select)
source_browse.setOnClickListener {
adapter.selectClickListener?.onSelectClick(adapterPosition)
}
}
fun bind(item: SourceItem) {
val source = item.source
setCardEdges(item)
// Set source name
title.text = source.name
// Set circle letter image.
itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
}
}

View File

@@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.ui.migration
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source
/**
* Item that contains source information.
*
* @param source Instance of [Source] containing source information.
* @param header The header for this item.
*/
data class SourceItem(val source: Source, val header: SelectionHeader? = null) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card_item
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as SourceAdapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

@@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.source.Source
data class ViewState(
val selectedSource: Source? = null,
val mangaForSource: List<MangaItem> = emptyList(),
val sourcesWithManga: List<SourceItem> = emptyList(),
val isReplacingManga: Boolean = false
)

View File

@@ -62,6 +62,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
with(image_view) {
setMaxTileSize((reader.activity as ReaderActivity).maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
setDoubleTapZoomDuration(reader.doubleTapAnimDuration.toInt())
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumScaleType(reader.scaleType)
setMinimumDpi(90)

View File

@@ -85,6 +85,12 @@ abstract class PagerReader : BaseReader() {
var cropBorders: Boolean = false
private set
/**
* Duration of the double tap animation
*/
var doubleTapAnimDuration = 500
private set
/**
* Scale type (fit width, fit screen, etc).
*/
@@ -166,6 +172,10 @@ abstract class PagerReader : BaseReader() {
.skip(1)
.distinctUntilChanged()
.subscribe { refreshAdapter() })
add(preferences.doubleTapAnimSpeed()
.asObservable()
.subscribe { doubleTapAnimDuration = it })
}
setPagesOnAdapter()

View File

@@ -57,6 +57,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
with(image_view) {
setMaxTileSize(readerActivity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
setDoubleTapZoomDuration(webtoonReader.doubleTapAnimDuration.toInt())
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
setMinimumDpi(90)

View File

@@ -59,6 +59,12 @@ class WebtoonReader : BaseReader() {
var cropBorders: Boolean = false
private set
/**
* Duration of the double tap animation
*/
var doubleTapAnimDuration = 500
private set
/**
* Gesture detector for image touch events.
*/
@@ -124,6 +130,10 @@ class WebtoonReader : BaseReader() {
.distinctUntilChanged()
.subscribe { refreshAdapter() })
subscriptions.add(readerActivity.preferences.doubleTapAnimSpeed()
.asObservable()
.subscribe { doubleTapAnimDuration = it })
setPagesOnAdapter()
return recycler
}

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Context
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v7.preference.*
@@ -10,7 +9,7 @@ import eu.kanade.tachiyomi.widget.preference.IntListPreference
@Target(AnnotationTarget.TYPE)
annotation class DSL
inline fun PreferenceManager.newScreen(context: Context, block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
return createPreferenceScreen(context).also { it.block() }
}

View File

@@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.toast
import rx.Subscription
@@ -51,9 +51,9 @@ class SettingsAboutController : SettingsController() {
onChange { newValue ->
val checked = newValue as Boolean
if (checked) {
UpdateCheckerJob.setupTask()
UpdaterJob.setupTask()
} else {
UpdateCheckerJob.cancelTask()
UpdaterJob.cancelTask()
}
true
}
@@ -131,7 +131,7 @@ class SettingsAboutController : SettingsController() {
if (appContext != null) {
// Start download
val url = args.getString(URL_KEY)
UpdateDownloaderService.downloadUpdate(appContext, url)
UpdaterService.downloadUpdate(appContext, url)
}
}
.build()

View File

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -26,10 +27,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.registerLocalReceiver
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -116,19 +114,22 @@ class SettingsBackupController : SettingsController() {
onClick {
val currentDir = preferences.backupsDirectory().getOrDefault()
val intent = if (Build.VERSION.SDK_INT < 21) {
try{
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Custom dir selected, open directory selector
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
preferences.context.getFilePicker(currentDir)
} else {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
}
} else {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, CODE_BACKUP_DIR)
} catch (e: ActivityNotFoundException){
//Fall back to custom picker on error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
}
}
startActivityForResult(intent, CODE_BACKUP_DIR)
}
preferences.backupsDirectory().asObservable()
@@ -204,25 +205,30 @@ class SettingsBackupController : SettingsController() {
fun createBackup(flags: Int) {
backupFlags = flags
// If API lower as KitKat use custom dir picker
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Get dirs
val preferences: PreferencesHelper = Injekt.get()
val currentDir = preferences.backupsDirectory().getOrDefault()
// Setup custom file picker intent
// Get dirs
val currentDir = preferences.backupsDirectory().getOrDefault()
Intent(activity, CustomLayoutPickerActivity::class.java)
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
} else {
// Use Androids build in file creator
Intent(Intent.ACTION_CREATE_DOCUMENT)
try {
// If API is lower than Lollipop use custom picker
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
preferences.context.getFilePicker(currentDir)
} else {
// Use Androids build in file creator
Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/*")
.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
}
startActivityForResult(intent, CODE_BACKUP_CREATE)
} catch (e: ActivityNotFoundException) {
// Handle errors where the android ROM doesn't support the built in picker
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE)
}
}
startActivityForResult(intent, CODE_BACKUP_CREATE)
}
class CreateBackupDialog : DialogController() {

View File

@@ -10,8 +10,11 @@ import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
@@ -55,9 +58,23 @@ abstract class SettingsController : PreferenceController() {
return preferenceScreen?.title?.toString()
}
override fun onAttach(view: View) {
fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) {
return
}
parentController = parentController.parentController
}
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
super.onAttach(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
super.onChangeStarted(handler, type)
}
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {

View File

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.getFilePicker
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -150,17 +152,19 @@ class SettingsDownloadController : SettingsController() {
}
fun customDirectorySelected(currentDir: String) {
if (Build.VERSION.SDK_INT < 21) {
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
startActivityForResult(i, DOWNLOAD_DIR_PRE_L)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_PRE_L)
} else {
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(i, DOWNLOAD_DIR_L)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
try {
startActivityForResult(intent, DOWNLOAD_DIR_L)
} catch (e: ActivityNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_L)
}
}
}
}

View File

@@ -62,6 +62,14 @@ class SettingsReaderController : SettingsController() {
defaultValue = "0"
summary = "%s"
}
intListPreference {
key = Keys.doubleTapAnimationSpeed
titleRes = R.string.pref_double_tap_anim_speed
entries = arrayOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R.string.double_tap_anim_speed_fast), context.getString(R.string.double_tap_anim_speed_normal))
entryValues = arrayOf("1", "250", "500") // using a value of 0 breaks the image viewer, so min is 1
defaultValue = "500"
summary = "%s"
}
switchPreference {
key = Keys.fullscreen
titleRes = R.string.pref_fullscreen