mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 13:08:56 +01:00
Downloading extensions from Github Repo. (#1101)
Downloading extensions from Github Repo.
This commit is contained in:
@@ -28,7 +28,7 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
||||
val top = child.bottom + params.bottomMargin
|
||||
val bottom = top + divider.intrinsicHeight
|
||||
val left = parent.paddingLeft + holder.margin
|
||||
val right = parent.paddingRight + holder.margin
|
||||
val right = parent.width - parent.paddingRight - holder.margin
|
||||
|
||||
divider.setBounds(left, top, right, bottom)
|
||||
divider.draw(c)
|
||||
@@ -41,4 +41,4 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
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 [ExtensionController].
|
||||
*/
|
||||
class ExtensionAdapter(val controller: ExtensionController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for browse item clicks.
|
||||
*/
|
||||
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller
|
||||
|
||||
interface OnButtonClickListener {
|
||||
fun onButtonClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import kotlinx.android.synthetic.main.extension_controller.*
|
||||
|
||||
|
||||
/**
|
||||
* Controller to manage the catalogues available in the app.
|
||||
*/
|
||||
open class ExtensionController : NucleusController<ExtensionPresenter>(),
|
||||
ExtensionAdapter.OnButtonClickListener,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
ExtensionTrustDialog.Listener {
|
||||
|
||||
/**
|
||||
* Adapter containing the list of manga from the catalogue.
|
||||
*/
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.label_extensions)
|
||||
}
|
||||
|
||||
override fun createPresenter(): ExtensionPresenter {
|
||||
return ExtensionPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.extension_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
ext_swipe_refresh.isRefreshing = true
|
||||
ext_swipe_refresh.refreshes().subscribeUntilDestroy {
|
||||
presenter.findAvailableExtensions()
|
||||
}
|
||||
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = ExtensionAdapter(this)
|
||||
// Create recycler and set adapter.
|
||||
ext_recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
ext_recycler.adapter = adapter
|
||||
ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
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(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)
|
||||
router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
private fun openTrustDialog(extension: Extension.Untrusted) {
|
||||
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName)
|
||||
.showDialog(router)
|
||||
}
|
||||
|
||||
fun setExtensions(extensions: List<ExtensionItem>) {
|
||||
ext_swipe_refresh?.isRefreshing = false
|
||||
adapter?.updateDataSet(extensions)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.*
|
||||
import android.support.v7.preference.internal.AbstractMultiSelectListPreference
|
||||
import android.support.v7.widget.DividerItemDecoration
|
||||
import android.support.v7.widget.DividerItemDecoration.VERTICAL
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.util.TypedValue
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import kotlinx.android.synthetic.main.extension_detail_controller.*
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
NucleusController<ExtensionDetailsPresenter>(bundle),
|
||||
PreferenceManager.OnDisplayPreferenceDialogListener,
|
||||
DialogPreference.TargetFragment,
|
||||
SourceLoginDialog.Listener {
|
||||
|
||||
private var lastOpenPreferencePosition: Int? = null
|
||||
|
||||
private var preferenceScreen: PreferenceScreen? = null
|
||||
|
||||
constructor(pkgName: String) : this(Bundle().apply {
|
||||
putString(PKGNAME_KEY, pkgName)
|
||||
})
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.extension_detail_controller, container, false)
|
||||
}
|
||||
|
||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY))
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_extension_info)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
val extension = presenter.extension
|
||||
val context = view.context
|
||||
|
||||
extension_title.text = extension.name
|
||||
extension_version.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||
extension_lang.text = context.getString(R.string.ext_language_info, extension.getLocalizedLang(context))
|
||||
extension_pkg.text = extension.pkgName
|
||||
extension.getApplicationIcon(context)?.let { extension_icon.setImageDrawable(it) }
|
||||
extension_uninstall_button.clicks().subscribeUntilDestroy {
|
||||
presenter.uninstallExtension()
|
||||
}
|
||||
|
||||
val themedContext by lazy { getPreferenceThemeContext() }
|
||||
val manager = PreferenceManager(themedContext)
|
||||
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
||||
manager.onDisplayPreferenceDialogListener = this
|
||||
val screen = manager.createPreferenceScreen(themedContext)
|
||||
preferenceScreen = screen
|
||||
|
||||
val multiSource = extension.sources.size > 1
|
||||
|
||||
for (source in extension.sources) {
|
||||
if (source is ConfigurableSource) {
|
||||
addPreferencesForSource(screen, source, multiSource)
|
||||
}
|
||||
}
|
||||
|
||||
manager.setPreferences(screen)
|
||||
|
||||
extension_prefs_recycler.layoutManager = LinearLayoutManager(context)
|
||||
extension_prefs_recycler.adapter = PreferenceGroupAdapter(screen)
|
||||
extension_prefs_recycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
||||
|
||||
if (screen.preferenceCount == 0) {
|
||||
extension_prefs_empty_view.show(R.drawable.ic_no_settings,
|
||||
R.string.ext_empty_preferences)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
preferenceScreen = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
fun onExtensionUninstalled() {
|
||||
router.popCurrentController()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
|
||||
}
|
||||
|
||||
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source, multiSource: Boolean) {
|
||||
val context = screen.context
|
||||
|
||||
// TODO
|
||||
val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) {
|
||||
source.preferences
|
||||
} else {*/
|
||||
context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
|
||||
/*}*/)
|
||||
|
||||
if (source is ConfigurableSource) {
|
||||
if (multiSource) {
|
||||
screen.preferenceCategory {
|
||||
title = source.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
||||
source.setupPreferenceScreen(newScreen)
|
||||
|
||||
for (i in 0 until newScreen.preferenceCount) {
|
||||
val pref = newScreen.getPreference(i)
|
||||
pref.preferenceDataStore = dataStore
|
||||
pref.order = Int.MAX_VALUE // reset to default order
|
||||
screen.addPreference(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferenceThemeContext(): Context {
|
||||
val tv = TypedValue()
|
||||
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
|
||||
return ContextThemeWrapper(activity, tv.resourceId)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (!isAttached) return
|
||||
|
||||
val screen = preference.parent!!
|
||||
|
||||
lastOpenPreferencePosition = (0 until screen.preferenceCount).indexOfFirst {
|
||||
screen.getPreference(it) === preference
|
||||
}
|
||||
|
||||
val f = when (preference) {
|
||||
is EditTextPreference -> EditTextPreferenceDialogController
|
||||
.newInstance(preference.getKey())
|
||||
is ListPreference -> ListPreferenceDialogController
|
||||
.newInstance(preference.getKey())
|
||||
is AbstractMultiSelectListPreference -> MultiSelectListPreferenceDialogController
|
||||
.newInstance(preference.getKey())
|
||||
else -> throw IllegalArgumentException("Tried to display dialog for unknown " +
|
||||
"preference type. Did you forget to override onDisplayPreferenceDialog()?")
|
||||
}
|
||||
f.targetController = this
|
||||
f.showDialog(router)
|
||||
}
|
||||
|
||||
override fun findPreference(key: CharSequence?): Preference {
|
||||
return preferenceScreen!!.getPreference(lastOpenPreferencePosition!!)
|
||||
}
|
||||
|
||||
override fun loginDialogClosed(source: LoginSource) {
|
||||
val lastOpen = lastOpenPreferencePosition ?: return
|
||||
(preferenceScreen?.getPreference(lastOpen) as? LoginPreference)?.notifyChanged()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PKGNAME_KEY = "pkg_name"
|
||||
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionDetailsPresenter(
|
||||
val pkgName: String,
|
||||
private val extensionManager: ExtensionManager = Injekt.get()
|
||||
) : BasePresenter<ExtensionDetailsController>() {
|
||||
|
||||
val extension = extensionManager.installedExtensions.first { it.pkgName == pkgName }
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
bindToUninstalledExtension()
|
||||
}
|
||||
|
||||
private fun bindToUninstalledExtension() {
|
||||
extensionManager.getInstalledExtensionsObservable()
|
||||
.skip(1)
|
||||
.filter { extensions -> extensions.none { it.pkgName == pkgName } }
|
||||
.map { Unit }
|
||||
.take(1)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onExtensionUninstalled()
|
||||
})
|
||||
}
|
||||
|
||||
fun uninstallExtension() {
|
||||
extensionManager.uninstallExtension(extension.pkgName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
|
||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val divider: Drawable
|
||||
|
||||
init {
|
||||
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 childCount = parent.childCount
|
||||
for (i in 0 until childCount - 1) {
|
||||
val child = parent.getChildAt(i)
|
||||
val holder = parent.getChildViewHolder(child)
|
||||
if (holder is ExtensionHolder &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder) {
|
||||
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.width - parent.paddingRight - holder.margin
|
||||
|
||||
divider.setBounds(left, top, right, bottom)
|
||||
divider.draw(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
|
||||
state: RecyclerView.State) {
|
||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import kotlinx.android.synthetic.main.extension_card_header.*
|
||||
|
||||
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
BaseFlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: ExtensionGroupItem) {
|
||||
title.text = when {
|
||||
item.installed -> itemView.context.getString(R.string.ext_installed)
|
||||
else -> itemView.context.getString(R.string.ext_available)
|
||||
} + " (" + item.size + ")"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Item that contains the language header.
|
||||
*
|
||||
* @param code The lang code.
|
||||
*/
|
||||
data class ExtensionGroupItem(val installed: Boolean, val size: Int) : AbstractHeaderItem<ExtensionGroupHolder>() {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.extension_card_header
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ExtensionGroupHolder {
|
||||
return ExtensionGroupHolder(view, adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ExtensionGroupHolder,
|
||||
position: Int, payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is ExtensionGroupItem) {
|
||||
return installed == other.installed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return installed.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||
import io.github.mthli.slice.Slice
|
||||
import kotlinx.android.synthetic.main.extension_card_item.*
|
||||
|
||||
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SlicedHolder {
|
||||
|
||||
override val slice = Slice(card).apply {
|
||||
setColor(adapter.cardBackground)
|
||||
}
|
||||
|
||||
override val viewToSlice: View
|
||||
get() = card
|
||||
|
||||
init {
|
||||
ext_button.setOnClickListener {
|
||||
adapter.buttonClickListener.onButtonClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: ExtensionItem) {
|
||||
val extension = item.extension
|
||||
setCardEdges(item)
|
||||
|
||||
// Set source name
|
||||
ext_title.text = extension.name
|
||||
version.text = extension.versionName
|
||||
lang.text = if (extension !is Extension.Untrusted) {
|
||||
extension.getLocalizedLang(itemView.context)
|
||||
} else {
|
||||
itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
||||
}
|
||||
|
||||
GlideApp.with(itemView.context).clear(image)
|
||||
if (extension is Extension.Available) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(extension.iconUrl)
|
||||
.into(image)
|
||||
} else {
|
||||
extension.getApplicationIcon(itemView.context)?.let { image.setImageDrawable(it) }
|
||||
}
|
||||
bindButton(item)
|
||||
}
|
||||
|
||||
fun bindButton(item: ExtensionItem) = with(ext_button) {
|
||||
isEnabled = true
|
||||
isClickable = true
|
||||
isActivated = false
|
||||
|
||||
val extension = item.extension
|
||||
|
||||
val installStep = item.installStep
|
||||
if (installStep != null) {
|
||||
setText(when (installStep) {
|
||||
InstallStep.Pending -> R.string.ext_pending
|
||||
InstallStep.Downloading -> R.string.ext_downloading
|
||||
InstallStep.Installing -> R.string.ext_installing
|
||||
InstallStep.Installed -> R.string.ext_installed
|
||||
InstallStep.Error -> R.string.action_retry
|
||||
})
|
||||
if (installStep != InstallStep.Error) {
|
||||
isEnabled = false
|
||||
isClickable = false
|
||||
}
|
||||
} else if (extension is Extension.Installed) {
|
||||
if (extension.hasUpdate) {
|
||||
isActivated = true
|
||||
setText(R.string.ext_update)
|
||||
} else {
|
||||
setText(R.string.ext_details)
|
||||
}
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
setText(R.string.ext_trust)
|
||||
} else {
|
||||
setText(R.string.ext_install)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
|
||||
/**
|
||||
* Item that contains source information.
|
||||
*
|
||||
* @param source Instance of [CatalogueSource] containing source information.
|
||||
* @param header The header for this item.
|
||||
*/
|
||||
data class ExtensionItem(val extension: Extension,
|
||||
val header: ExtensionGroupItem? = null,
|
||||
val installStep: InstallStep? = null) :
|
||||
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.extension_card_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ExtensionHolder {
|
||||
return ExtensionHolder(view, adapter as ExtensionAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ExtensionHolder,
|
||||
position: Int, payloads: List<Any?>?) {
|
||||
|
||||
if (payloads == null || payloads.isEmpty()) {
|
||||
holder.bind(this)
|
||||
} else {
|
||||
holder.bindButton(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return extension.pkgName == (other as ExtensionItem).extension.pkgName
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return extension.pkgName.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private typealias ExtensionTuple
|
||||
= Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||
|
||||
/**
|
||||
* Presenter of [ExtensionController].
|
||||
*/
|
||||
open class ExtensionPresenter(
|
||||
private val extensionManager: ExtensionManager = Injekt.get()
|
||||
) : BasePresenter<ExtensionController>() {
|
||||
|
||||
private var extensions = emptyList<ExtensionItem>()
|
||||
|
||||
private var currentDownloads = hashMapOf<String, InstallStep>()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
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())
|
||||
.subscribeLatestCache({ view, _ -> view.setExtensions(extensions) })
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||
val (installed, untrusted, available) = tuple
|
||||
|
||||
val items = mutableListOf<ExtensionItem>()
|
||||
|
||||
val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { it.name }))
|
||||
val untrustedSorted = untrusted.sortedBy { it.name }
|
||||
val availableSorted = available
|
||||
// Filter out already installed extensions
|
||||
.filter { avail -> installed.none { it.pkgName == avail.pkgName }
|
||||
&& untrusted.none { it.pkgName == avail.pkgName } }
|
||||
.sortedBy { it.name }
|
||||
|
||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||
val header = ExtensionGroupItem(true, 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 header = ExtensionGroupItem(false, availableSorted.size)
|
||||
items += availableSorted.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
}
|
||||
}
|
||||
|
||||
this.extensions = items
|
||||
return items
|
||||
}
|
||||
|
||||
@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.put(extension.pkgName, it) }
|
||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||
.map { state -> updateInstallStep(extension, state) }
|
||||
.subscribeWithView({ view, item ->
|
||||
if (item != null) {
|
||||
view.downloadUpdate(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun uninstallExtension(pkgName: String) {
|
||||
extensionManager.uninstallExtension(pkgName)
|
||||
}
|
||||
|
||||
fun findAvailableExtensions() {
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
|
||||
fun trustSignature(signatureHash: String) {
|
||||
extensionManager.trustSignature(signatureHash)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
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
|
||||
|
||||
class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T: ExtensionTrustDialog.Listener {
|
||||
|
||||
constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply {
|
||||
putString(SIGNATURE_KEY, signatureHash)
|
||||
putString(PKGNAME_KEY, pkgName)
|
||||
}) {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.untrusted_extension)
|
||||
.content(R.string.untrusted_extension_message)
|
||||
.positiveText(R.string.ext_trust)
|
||||
.negativeText(R.string.ext_uninstall)
|
||||
.onPositive { _, _ ->
|
||||
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY))
|
||||
}
|
||||
.onNegative { _, _ ->
|
||||
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY))
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SIGNATURE_KEY = "signature_key"
|
||||
const val PKGNAME_KEY = "pkgname_key"
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun trustSignature(signatureHash: String)
|
||||
fun uninstallExtension(pkgName: String)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import java.util.*
|
||||
|
||||
fun Extension.getLocalizedLang(context: Context): String {
|
||||
return when (lang) {
|
||||
null -> ""
|
||||
"" -> context.getString(R.string.other_source)
|
||||
"all" -> context.getString(R.string.all_lang)
|
||||
else -> {
|
||||
val locale = Locale(lang)
|
||||
locale.getDisplayName(locale).capitalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Extension.getApplicationIcon(context: Context): Drawable? {
|
||||
return try {
|
||||
context.packageManager.getApplicationIcon(pkgName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||
@@ -80,6 +81,7 @@ class MainActivity : BaseActivity() {
|
||||
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
|
||||
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
|
||||
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
|
||||
R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id)
|
||||
R.id.nav_drawer_downloads -> {
|
||||
router.pushController(DownloadController().withFadeTransaction())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user