Extensions now in a bottom sheet on the browse tab

Boy I love bottom sheets guys
This commit is contained in:
Jay 2020-03-11 00:28:47 -07:00
parent 1f91ad0a07
commit e68a1ae48d
19 changed files with 675 additions and 63 deletions

View File

@ -13,7 +13,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -26,17 +26,20 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.extension.SettingsExtensionsController
import eu.kanade.tachiyomi.ui.main.RootSearchInterface import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.catalogue_main_controller.* import kotlinx.android.synthetic.main.catalogue_main_controller.*
import kotlinx.android.synthetic.main.extensions_bottom_sheet.*
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.math.max
/** /**
* This controller shows and manages the different catalogues enabled by the user. * This controller shows and manages the different catalogues enabled by the user.
@ -62,6 +65,13 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
*/ */
private var adapter: CatalogueAdapter? = null private var adapter: CatalogueAdapter? = null
var extQuery = ""
private set
var headerHeight = 0
var customTitle = ""
/** /**
* Called when controller is initialized. * Called when controller is initialized.
*/ */
@ -76,7 +86,9 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
* @return title. * @return title.
*/ */
override fun getTitle(): String? { override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues) return if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED)
applicationContext?.getString(R.string.label_extensions)
else applicationContext?.getString(R.string.label_catalogues)
} }
/** /**
@ -114,11 +126,41 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context) recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
recycler.adapter = adapter recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context)) recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) val attrsArray = intArrayOf(android.R.attr.actionBarSize)
val array = view.context.obtainStyledAttributes(attrsArray)
scrollViewWith(recycler) val appBarHeight = array.getDimensionPixelSize(0, 0)
array.recycle()
scrollViewWith(recycler) {
headerHeight = it.systemWindowInsetTop + appBarHeight
}
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
ext_bottom_sheet.onCreate(this)
ext_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior
.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, progress: Float) {
shadow2.alpha = (1 - max(0f, progress)) * 0.25f
sheet_layout.alpha = 1 - progress
activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress))
}
override fun onStateChanged(p0: View, state: Int) {
if (state == BottomSheetBehavior.STATE_EXPANDED) activity?.appbar?.y = 0f
if (state == BottomSheetBehavior.STATE_EXPANDED ||
state == BottomSheetBehavior.STATE_COLLAPSED)
sheet_layout.alpha =
if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f
retainViewMode = if (state == BottomSheetBehavior.STATE_EXPANDED)
RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH
activity?.invalidateOptionsMenu()
setTitle()
sheet_layout.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED
sheet_layout.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED
}
})
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
@ -129,6 +171,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) { if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
ext_bottom_sheet.updateExtTitle()
presenter.updateSources() presenter.updateSources()
} }
} }
@ -192,6 +235,25 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) {
// Inflate menu
inflater.inflate(R.menu.extension_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.search_extensions)
// Create query listener which opens the global search view.
setOnQueryTextChangeListener(searchView) {
extQuery = it ?: ""
ext_bottom_sheet.drawExtensions()
true
}
}
else {
// Inflate menu // Inflate menu
inflater.inflate(R.menu.catalogue_main, menu) inflater.inflate(R.menu.catalogue_main, menu)
@ -203,9 +265,11 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view. // Create query listener which opens the global search view.
searchView.queryTextChangeEvents() setOnQueryTextChangeListener(searchView, true) {
.filter { it.isSubmitted } if (!it.isNullOrBlank()) performGlobalSearch(it)
.subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) } true
}
}
} }
private fun performGlobalSearch(query: String){ private fun performGlobalSearch(query: String){
@ -222,9 +286,18 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
when (item.itemId) { when (item.itemId) {
// Initialize option to open catalogue settings. // Initialize option to open catalogue settings.
R.id.action_filter -> { R.id.action_filter -> {
router.pushController((RouterTransaction.with(SettingsSourcesController())) val controller =
.popChangeHandler(SettingsSourcesFadeChangeHandler()) if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED)
.pushChangeHandler(FadeChangeHandler())) SettingsExtensionsController()
else SettingsSourcesController()
router.pushController(
(RouterTransaction.with(controller)).popChangeHandler(
SettingsSourcesFadeChangeHandler()
).pushChangeHandler(FadeChangeHandler())
)
}
R.id.action_dismiss -> {
ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }

View File

@ -12,7 +12,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.* import java.util.TreeMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**

View File

@ -3,17 +3,19 @@ package eu.kanade.tachiyomi.ui.extension
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* Adapter that holds the catalogue cards. * Adapter that holds the catalogue cards.
* *
* @param controller instance of [ExtensionController]. * @param listener instance of [OnButtonClickListener].
*/ */
class ExtensionAdapter(val controller: ExtensionController) : class ExtensionAdapter(val listener: OnButtonClickListener) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) { FlexibleAdapter<IFlexible<*>>(null, listener, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) val cardBackground = (listener as ExtensionBottomSheet).context.getResourceColor(R.attr
.background_card)
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
@ -22,7 +24,7 @@ class ExtensionAdapter(val controller: ExtensionController) :
/** /**
* Listener for browse item clicks. * Listener for browse item clicks.
*/ */
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener
interface OnButtonClickListener { interface OnButtonClickListener {
fun onButtonClick(position: Int) fun onButtonClick(position: Int)

View File

@ -0,0 +1,153 @@
package eu.kanade.tachiyomi.ui.extension
import android.app.Application
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
/**
* Presenter of [ExtensionController].
*/
open class ExtensionBottomPresenter(
private val bottomSheet: ExtensionBottomSheet,
private val extensionManager: ExtensionManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : CoroutineScope {
override var coroutineContext: CoroutineContext = Job() + Dispatchers.Default
private var extensions = emptyList<ExtensionItem>()
private var currentDownloads = hashMapOf<String, InstallStep>()
fun onCreate() {
extensionManager.findAvailableExtensions()
bindToExtensionsObservable()
}
private fun bindToExtensionsObservable(): Subscription {
val installedObservable = extensionManager.getInstalledExtensionsObservable()
val untrustedObservable = extensionManager.getUntrustedExtensionsObservable()
val availableObservable = extensionManager.getAvailableExtensionsObservable()
.startWith(emptyList<Extension.Available>())
return Observable.combineLatest(installedObservable, untrustedObservable, availableObservable)
{ installed, untrusted, available -> Triple(installed, untrusted, available) }
.debounce(100, TimeUnit.MILLISECONDS)
.map(::toItems)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
bottomSheet.setExtensions(extensions)
}
}
@Synchronized
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().getOrDefault()
val (installed, untrusted, available) = tuple
val items = mutableListOf<ExtensionItem>()
val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { !it.isObsolete }, { it.pkgName }))
val untrustedSorted = untrusted.sortedBy { it.pkgName }
val availableSorted = available
// Filter out already installed extensions and disabled languages
.filter { avail -> installed.none { it.pkgName == avail.pkgName }
&& untrusted.none { it.pkgName == avail.pkgName }
&& (avail.lang in activeLangs || avail.lang == "all")}
.sortedBy { it.pkgName }
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
items += installedSorted.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
}
items += untrustedSorted.map { extension ->
ExtensionItem(extension, header)
}
}
if (availableSorted.isNotEmpty()) {
val availableGroupedByLang = availableSorted
.groupBy { LocaleHelper.getDisplayName(it.lang, context) }
.toSortedMap()
availableGroupedByLang
.forEach {
val header = ExtensionGroupItem(it.key, it.value.size)
items += it.value.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
}
}
}
this.extensions = items
return items
}
fun getExtensionUpdateCount():Int = preferences.extensionUpdatesCount().getOrDefault()
fun getAutoCheckPref() = preferences.automaticExtUpdates()
@Synchronized
private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? {
val extensions = extensions.toMutableList()
val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
return if (position != -1) {
val item = extensions[position].copy(installStep = state)
extensions[position] = item
this.extensions = extensions
item
} else {
null
}
}
fun installExtension(extension: Extension.Available) {
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
}
fun updateExtension(extension: Extension.Installed) {
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
}
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
this.doOnNext { currentDownloads[extension.pkgName] = it }
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
.map { state -> updateInstallStep(extension, state) }
.subscribe { item ->
if (item != null) {
bottomSheet.downloadUpdate(item)
}
}
}
fun uninstallExtension(pkgName: String) {
extensionManager.uninstallExtension(pkgName)
}
fun findAvailableExtensions() {
extensionManager.findAvailableExtensions()
}
fun trustSignature(signatureHash: String) {
extensionManager.trustSignature(signatureHash)
}
}

View File

@ -0,0 +1,224 @@
package eu.kanade.tachiyomi.ui.extension
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.LinearLayout
import com.f2prateek.rx.preferences.Preference
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import kotlinx.android.synthetic.main.extensions_bottom_sheet.view.*
class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: LinearLayout(context, attrs),
ExtensionAdapter.OnButtonClickListener,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
ExtensionTrustDialog.Listener {
var sheetBehavior: BottomSheetBehavior<*>? = null
lateinit var autoCheckItem:AutoCheckItem
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
val presenter = ExtensionBottomPresenter(this)
private var extensions: List<ExtensionItem> = emptyList()
lateinit var controller: CatalogueController
fun onCreate(controller: CatalogueController) {
// Initialize adapter, scroll listener and recycler views
autoCheckItem = AutoCheckItem(presenter.getAutoCheckPref())
adapter = ExtensionAdapter(this)
sheetBehavior = BottomSheetBehavior.from(this)
// Create recycler and set adapter.
ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
ext_recycler.adapter = adapter
ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(context))
ext_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
// scrollViewWith(ext_recycler, true, ext_swipe_refresh)
this.controller = controller
//ext_swipe_refresh.refreshes().subscribeUntilDestroy {
// presenter.findAvailableExtensions()
presenter.onCreate()
updateExtTitle()
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
val array = context.obtainStyledAttributes(attrsArray)
val headerHeight = array.getDimensionPixelSize(0, 0)
array.recycle()
ext_recycler.doOnApplyWindowInsets { _, windowInsets, _ ->
ext_recycler.updateLayoutParams<LayoutParams> {
topMargin = windowInsets.systemWindowInsetTop + headerHeight -
(sheet_layout.height)
}
}
sheet_layout.setOnClickListener {
if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED) {
sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
} else {
sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
presenter.getExtensionUpdateCount()
}
fun updateExtTitle() {
val extCount = presenter.getExtensionUpdateCount()
title_text.text = if (extCount == 0) context.getString(R.string.label_extensions)
else resources.getQuantityString(R.plurals.extensions_updates_available, extCount,
extCount)
}
override fun onButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
when (extension) {
is Extension.Installed -> {
if (!extension.hasUpdate) {
openDetails(extension)
} else {
presenter.updateExtension(extension)
}
}
is Extension.Available -> {
presenter.installExtension(extension)
}
is Extension.Untrusted -> {
openTrustDialog(extension)
}
}
}
override fun onItemClick(view: View?, position: Int): Boolean {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
if (extension is Extension.Installed) {
openDetails(extension)
} else if (extension is Extension.Untrusted) {
openTrustDialog(extension)
}
return false
}
override fun onItemLongClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
if (extension is Extension.Installed || extension is Extension.Untrusted) {
uninstallExtension(extension.pkgName)
}
}
private fun openDetails(extension: Extension.Installed) {
val controller = ExtensionDetailsController(extension.pkgName)
this.controller.router.pushController(controller.withFadeTransaction())
}
private fun openTrustDialog(extension: Extension.Untrusted) {
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName)
.showDialog(controller.router)
}
fun setExtensions(extensions: List<ExtensionItem>) {
//ext_swipe_refresh?.isRefreshing = false
this.extensions = extensions
controller.presenter.updateSources()
drawExtensions()
}
fun drawExtensions() {
if (!controller.extQuery.isBlank()) {
adapter?.updateDataSet(
extensions.filter {
it.extension.name.contains(controller.extQuery, ignoreCase = true)
})
} else {
adapter?.updateDataSet(extensions)
}
updateExtTitle()
setLastUsedSource()
}
/**
* Called to set the last used catalogue at the top of the view.
*/
private fun setLastUsedSource() {
adapter?.removeAllScrollableHeaders()
adapter?.addScrollableHeader(autoCheckItem)
}
fun downloadUpdate(item: ExtensionItem) {
adapter?.updateItem(item, item.installStep)
}
override fun trustSignature(signatureHash: String) {
presenter.trustSignature(signatureHash)
}
override fun uninstallExtension(pkgName: String) {
presenter.uninstallExtension(pkgName)
}
}
class AutoCheckItem(private val autoCheck: Preference<Boolean>) : AbstractHeaderItem<AutoCheckItem.AutoCheckHolder>() {
override fun getLayoutRes(): Int {
return R.layout.auto_ext_checkbox
}
override fun createViewHolder(
view: View, adapter: FlexibleAdapter<IFlexible<*>>
): AutoCheckHolder {
return AutoCheckHolder(view, adapter, autoCheck)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>>,
holder: AutoCheckHolder,
position: Int,
payloads: MutableList<Any?>?
) {
//holder.bind(autoCheck.getOrDefault())
}
override fun equals(other: Any?): Boolean {
return (this === other)
}
override fun hashCode(): Int {
return -1
}
class AutoCheckHolder(val view: View, private val adapter: FlexibleAdapter<IFlexible<*>>,
autoCheck: Preference<Boolean>) :
FlexibleViewHolder(view, adapter, true) {
private val autoCheckbox: CheckBox = view.findViewById(R.id.auto_checkbox)
init {
autoCheckbox.bindToPreference(autoCheck)
}
/**
* Binds a checkbox or switch view with a boolean preference.
*/
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
isChecked = pref.getOrDefault()
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
}
}
}

View File

@ -17,7 +17,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private typealias ExtensionTuple typealias ExtensionTuple
= Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>> = Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
/** /**

View File

@ -3,18 +3,18 @@ package eu.kanade.tachiyomi.ui.extension
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle) class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T: ExtensionTrustDialog.Listener { where T: ExtensionTrustDialog.Listener {
lateinit var listener: Listener
constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply { constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply {
putString(SIGNATURE_KEY, signatureHash) putString(SIGNATURE_KEY, signatureHash)
putString(PKGNAME_KEY, pkgName) putString(PKGNAME_KEY, pkgName)
}) { }) {
targetController = target listener = target
} }
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
@ -22,10 +22,10 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
.title(R.string.untrusted_extension) .title(R.string.untrusted_extension)
.message(R.string.untrusted_extension_message) .message(R.string.untrusted_extension_message)
.positiveButton(R.string.ext_trust) { .positiveButton(R.string.ext_trust) {
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!) listener.trustSignature(args.getString(SIGNATURE_KEY)!!)
} }
.negativeButton(R.string.ext_uninstall) { .negativeButton(R.string.ext_uninstall) {
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!) listener.uninstallExtension(args.getString(PKGNAME_KEY)!!)
} }
} }

View File

@ -8,7 +8,6 @@ import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.extension.ExtensionController
import eu.kanade.tachiyomi.ui.migration.MigrationController import eu.kanade.tachiyomi.ui.migration.MigrationController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@ -24,13 +23,6 @@ class SettingsMainController : SettingsController() {
val tintColor = context.getResourceColor(R.attr.colorAccent) val tintColor = context.getResourceColor(R.attr.colorAccent)
extensionPreference {
iconRes = R.drawable.ic_extension_black_24dp
iconTint = tintColor
titleRes = R.string.label_extensions
onClick { navigateTo(ExtensionController()) }
}
preference { preference {
iconRes = R.drawable.ic_tune_white_24dp iconRes = R.drawable.ic_tune_white_24dp
iconTint = tintColor iconTint = tintColor

View File

@ -294,10 +294,12 @@ data class ViewPaddingState(
) )
fun Controller.setOnQueryTextChangeListener(searchView: SearchView, f: (text: String?) -> Boolean) { fun Controller.setOnQueryTextChangeListener(searchView: SearchView, onlyOnSubmit:Boolean = false,
f: (text: String?) -> Boolean) {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) { if (!onlyOnSubmit && router.backstack.lastOrNull()?.controller() ==
this@setOnQueryTextChangeListener) {
return f(newText) return f(newText)
} }
return false return false
@ -324,10 +326,10 @@ fun Controller.scrollViewWith(recycler: RecyclerView,
val headerHeight = insets.systemWindowInsetTop + array.getDimensionPixelSize(0, 0) val headerHeight = insets.systemWindowInsetTop + array.getDimensionPixelSize(0, 0)
view.updatePaddingRelative( view.updatePaddingRelative(
top = headerHeight, top = headerHeight,
bottom = if (padBottom) insets.systemWindowInsetBottom else 0 bottom = if (padBottom) insets.systemWindowInsetBottom else view.paddingBottom
) )
swipeRefreshLayout?.setProgressViewOffset(false, headerHeight + (-60).dpToPx, swipeRefreshLayout?.setProgressViewOffset(false, headerHeight + (-60).dpToPx,
headerHeight + 10.dpToPx) headerHeight)
statusBarHeight = insets.systemWindowInsetTop statusBarHeight = insets.systemWindowInsetTop
array.recycle() array.recycle()
f?.invoke(insets) f?.invoke(insets)

View File

@ -28,7 +28,7 @@ class ExtensionPreference @JvmOverloads constructor(context: Context, attrs: Att
val updates = Injekt.get<PreferencesHelper>().extensionUpdatesCount().getOrDefault() val updates = Injekt.get<PreferencesHelper>().extensionUpdatesCount().getOrDefault()
if (updates > 0) { if (updates > 0) {
extUpdateText.text = context.resources.getQuantityString(R.plurals extUpdateText.text = context.resources.getQuantityString(R.plurals
.extensions_updates_available, updates, updates) .updates_available, updates, updates)
extUpdateText.visible() extUpdateText.visible()
} }
else { else {

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
android:alpha="0.12" android:color="?attr/colorOnSurface" />
<item android:color="?colorPrimary" />
</selector>

View File

@ -2,6 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:tint="?actionBarTintColor"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/auto_checkbox"
android:layout_marginStart="8dp"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_auto_check_extensions">
</com.google.android.material.checkbox.MaterialCheckBox>

View File

@ -1,15 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:layout_marginBottom="30dp"
android:paddingBottom="20dp"
tools:listitem="@layout/catalogue_main_controller_card" /> tools:listitem="@layout/catalogue_main_controller_card" />
</FrameLayout> </FrameLayout>
<View
android:id="@+id/shadow"
android:layout_width="match_parent"
android:layout_height="24dp"
android:alpha="0.5"
android:background="@drawable/shape_gradient_top_shadow"
android:paddingBottom="10dp"
app:layout_anchorGravity="top"
app:layout_anchor="@id/ext_bottom_sheet" />
<!-- Adding bottom sheet after main content -->
<include layout="@layout/extensions_bottom_sheet"/>
<View
android:id="@+id/shadow2"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_gravity="bottom"
android:alpha="0.25"
android:background="@drawable/shape_gradient_top_shadow" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="@dimen/material_component_lists_two_line_height"
android:background="?attr/selectable_list_drawable">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginStart="12dp"
android:paddingStart="0dp"
android:paddingEnd="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/source_browse"
tools:text="Source title" />
<Button
android:id="@+id/source_browse"
style="@style/Theme.Widget.Button.Borderless.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update_check_look_for_updates"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.extension.ExtensionBottomSheet 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"
android:id="@+id/ext_bottom_sheet"
style="@style/BottomSheetDialogTheme"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_bottom_sheet_dialog_fragment"
android:backgroundTint="?android:attr/colorBackground"
android:orientation="vertical"
app:behavior_peekHeight="48sp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:id="@+id/sheet_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_bottom_sheet_dialog_fragment"
android:backgroundTint="?android:attr/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/pill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:alpha="0.25"
android:contentDescription="@string/drag_handle"
android:src="@drawable/draggable_pill"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title_text"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textAlignment="center"
android:textColor="?actionBarTintColor"
android:textSize="18sp"
tools:text="Extensions" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ext_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground"
android:clipToPadding="false"
tools:listitem="@layout/extension_card_header" />
</eu.kanade.tachiyomi.ui.extension.ExtensionBottomSheet>

View File

@ -4,6 +4,7 @@
<item <item
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_search_white_24dp"
android:visible="false"
android:title="@string/action_search" android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|ifRoom" /> app:showAsAction="collapseActionView|ifRoom" />
@ -12,13 +13,20 @@
android:id="@+id/action_filter" android:id="@+id/action_filter"
android:title="@string/action_filter" android:title="@string/action_filter"
android:icon="@drawable/ic_filter_list_white_24dp" android:icon="@drawable/ic_filter_list_white_24dp"
app:showAsAction="ifRoom"/> app:showAsAction="always"/>
<item <item
android:id="@+id/action_auto_check" android:id="@+id/action_auto_check"
android:title="@string/action_auto_check_extensions" android:title="@string/action_auto_check_extensions"
android:icon="@drawable/ic_filter_list_white_24dp" android:icon="@drawable/ic_sync_black_24dp"
android:visible="false"
android:checkable="true" android:checkable="true"
app:showAsAction="never"/> app:showAsAction="never"/>
<item
android:id="@+id/action_dismiss"
android:title="@string/action_dismiss"
android:icon="@drawable/ic_close_white_24dp"
app:showAsAction="always"/>
</menu> </menu>

View File

@ -27,10 +27,14 @@
<string name="label_help">Help</string> <string name="label_help">Help</string>
<string name="unlock">Unlock</string> <string name="unlock">Unlock</string>
<string name="unlock_library">Unlock to access Library</string> <string name="unlock_library">Unlock to access Library</string>
<plurals name="extensions_updates_available"> <plurals name="updates_available">
<item quantity="one">Update available</item> <item quantity="one">Update available</item>
<item quantity="other">%d updates available</item> <item quantity="other">%d updates available</item>
</plurals> </plurals>
<plurals name="extensions_updates_available">
<item quantity="one">Extension update available</item>
<item quantity="other">%d extension updates available</item>
</plurals>
<plurals name="downloads_pending"> <plurals name="downloads_pending">
<item quantity="one">1 in queue</item> <item quantity="one">1 in queue</item>
<item quantity="other">%d in queue</item> <item quantity="other">%d in queue</item>
@ -139,6 +143,7 @@
<string name="action_sort_by">Sort category by…</string> <string name="action_sort_by">Sort category by…</string>
<string name="action_switch">Switch</string> <string name="action_switch">Switch</string>
<string name="action_bug_report">Report a Bug</string> <string name="action_bug_report">Report a Bug</string>
<string name="action_dismiss">Dismiss</string>
<!-- Operations --> <!-- Operations -->
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
@ -474,6 +479,7 @@
<string name="latest">Latest</string> <string name="latest">Latest</string>
<string name="browse">Browse</string> <string name="browse">Browse</string>
<string name="in_library">In Library</string> <string name="in_library">In Library</string>
<string name="search_extensions">Search extensions…</string>
<!-- Manga activity --> <!-- Manga activity -->
<string name="manga_not_in_db">This manga has been removed from the database.</string> <string name="manga_not_in_db">This manga has been removed from the database.</string>