mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-06-02 06:03:44 +02:00
Migrate MagiskDialog to Compose and remove kapt/data binding
Replace MagiskDialog from AppCompatDialog with data binding to a Compose state holder rendered via miuix SuperDialog. Update MarkDownDialog to use AndroidView-based Compose content. Remove kapt plugin, data binding, ObservableHost, RvItemAdapter, DataBindingAdapters, and all remaining XML layouts (dialog_magisk_base, item_list_single_line, markdown_window_md2). Clean up dead version catalog entries and remove UIActivity generic type parameter. Made-with: Cursor
This commit is contained in:
@@ -3,23 +3,12 @@ plugins {
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.compose")
|
||||
kotlin("plugin.serialization")
|
||||
alias(libs.plugins.legacy.kapt)
|
||||
}
|
||||
|
||||
setupMainApk()
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
mapDiagnosticLocations = true
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", "1000")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
@@ -46,8 +35,6 @@ dependencies {
|
||||
implementation(libs.rikka.layoutinflater)
|
||||
implementation(libs.rikka.insets)
|
||||
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
@@ -67,7 +54,4 @@ dependencies {
|
||||
implementation(libs.navigation3.runtime)
|
||||
implementation(libs.navigationevent.compose)
|
||||
implementation(libs.lifecycle.viewmodel.navigation3)
|
||||
|
||||
// Make sure kapt runs with a proper kotlin-stdlib
|
||||
kapt(kotlin("stdlib"))
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.events.DialogEvent
|
||||
@@ -20,9 +18,7 @@ import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
@@ -22,16 +21,12 @@ import com.topjohnwu.magisk.core.wrap
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||
|
||||
abstract class UIActivity<Binding : ViewDataBinding>
|
||||
abstract class UIActivity
|
||||
: AppCompatActivity(), ViewModelHolder, IActivityExtension {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
override val extension = ActivityExtension(this)
|
||||
|
||||
protected val binded get() = ::binding.isInitialized
|
||||
|
||||
open val snackbarView get() = binding.root
|
||||
abstract val snackbarView: View
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
init {
|
||||
@@ -48,7 +43,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
|
||||
|
||||
extension.onCreate(savedInstanceState)
|
||||
if (isRunningAsStub) {
|
||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||
val delegate = delegate
|
||||
val clz = delegate.javaClass
|
||||
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||
@@ -59,8 +53,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
|
||||
|
||||
startObserveLiveData()
|
||||
|
||||
// We need to set the window background explicitly since for whatever reason it's not
|
||||
// propagated upstream
|
||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
.use { it.getDrawable(0) }
|
||||
.also { window.setBackgroundDrawable(it) }
|
||||
@@ -69,7 +61,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
window?.decorView?.post {
|
||||
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
|
||||
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
|
||||
@@ -13,6 +13,6 @@ interface ContextExecutor {
|
||||
}
|
||||
|
||||
interface ActivityExecutor {
|
||||
operator fun invoke(activity: UIActivity<*>)
|
||||
operator fun invoke(activity: UIActivity)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Spanned
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@BindingAdapter("gone")
|
||||
fun setGone(view: View, gone: Boolean) {
|
||||
view.isGone = gone
|
||||
}
|
||||
|
||||
@BindingAdapter("invisible")
|
||||
fun setInvisible(view: View, invisible: Boolean) {
|
||||
view.isInvisible = invisible
|
||||
}
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
fun setGoneUnless(view: View, goneUnless: Boolean) {
|
||||
setGone(view, goneUnless.not())
|
||||
}
|
||||
|
||||
@BindingAdapter("invisibleUnless")
|
||||
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
||||
setInvisible(view, invisibleUnless.not())
|
||||
}
|
||||
|
||||
@BindingAdapter("markdownText")
|
||||
fun setMarkdownText(tv: TextView, markdown: Spanned) {
|
||||
ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
|
||||
}
|
||||
|
||||
@BindingAdapter("onNavigationClick")
|
||||
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
|
||||
view.setNavigationOnClickListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
|
||||
view.setImageResource(resId)
|
||||
}
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
fun setImageResource(view: ImageView, drawable: Drawable) {
|
||||
view.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
@BindingAdapter("onTouch")
|
||||
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
|
||||
view.setOnTouchListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("scrollToLast")
|
||||
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
|
||||
|
||||
fun scrollToLast() = UiThreadHandler.handler.postDelayed({
|
||||
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
|
||||
}, 30)
|
||||
|
||||
fun wait(callback: () -> Unit) {
|
||||
UiThreadHandler.handler.postDelayed(callback, 1000)
|
||||
}
|
||||
|
||||
fun RecyclerView.Adapter<*>.setListener() {
|
||||
val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
scrollToLast()
|
||||
}
|
||||
}
|
||||
registerAdapterDataObserver(observer)
|
||||
view.setTag(R.id.recyclerScrollListener, observer)
|
||||
}
|
||||
|
||||
fun RecyclerView.Adapter<*>.removeListener() {
|
||||
val observer =
|
||||
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
|
||||
unregisterAdapterDataObserver(observer)
|
||||
}
|
||||
|
||||
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
|
||||
|
||||
if (shouldScrollToLast) {
|
||||
trySetListener()
|
||||
} else {
|
||||
view.adapter?.removeListener()
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("isEnabled")
|
||||
fun setEnabled(view: View, isEnabled: Boolean) {
|
||||
view.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
@BindingAdapter("error")
|
||||
fun TextInputLayout.setErrorString(error: String) {
|
||||
val newError = error.let { if (it.isEmpty()) null else it }
|
||||
if (this.error == null && newError == null) return
|
||||
this.error = newError
|
||||
}
|
||||
|
||||
// md2
|
||||
|
||||
@BindingAdapter(
|
||||
"android:layout_marginLeft",
|
||||
"android:layout_marginTop",
|
||||
"android:layout_marginRight",
|
||||
"android:layout_marginBottom",
|
||||
"android:layout_marginStart",
|
||||
"android:layout_marginEnd",
|
||||
requireAll = false
|
||||
)
|
||||
fun View.setMargins(
|
||||
marginLeft: Int?,
|
||||
marginTop: Int?,
|
||||
marginRight: Int?,
|
||||
marginBottom: Int?,
|
||||
marginStart: Int?,
|
||||
marginEnd: Int?
|
||||
) = updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginLeft?.let { leftMargin = it }
|
||||
marginTop?.let { topMargin = it }
|
||||
marginRight?.let { rightMargin = it }
|
||||
marginBottom?.let { bottomMargin = it }
|
||||
marginStart?.let { this.marginStart = it }
|
||||
marginEnd?.let { this.marginEnd = it }
|
||||
}
|
||||
|
||||
@BindingAdapter("nestedScrollingEnabled")
|
||||
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
|
||||
isNestedScrollingEnabled = enabled
|
||||
}
|
||||
|
||||
@BindingAdapter("isSelected")
|
||||
fun View.isSelected(isSelected: Boolean) {
|
||||
this.isSelected = isSelected
|
||||
}
|
||||
|
||||
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
|
||||
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
|
||||
if (dividerHorizontal != null) {
|
||||
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
|
||||
setDrawable(dividerHorizontal)
|
||||
}.let { addItemDecoration(it) }
|
||||
}
|
||||
if (dividerVertical != null) {
|
||||
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
|
||||
setDrawable(dividerVertical)
|
||||
}.let { addItemDecoration(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("icon")
|
||||
fun Button.setIconRes(res: Int) {
|
||||
(this as MaterialButton).setIconResource(res)
|
||||
}
|
||||
|
||||
@BindingAdapter("icon")
|
||||
fun Button.setIcon(drawable: Drawable) {
|
||||
(this as MaterialButton).icon = drawable
|
||||
}
|
||||
|
||||
@BindingAdapter("strokeWidth")
|
||||
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
|
||||
strokeWidth = stroke.roundToInt()
|
||||
}
|
||||
|
||||
@BindingAdapter("onMenuClick")
|
||||
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
|
||||
setOnMenuItemClickListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("progressAnimated")
|
||||
fun ProgressBar.setProgressAnimated(newProgress: Int) {
|
||||
val animator = tag as? ValueAnimator
|
||||
animator?.cancel()
|
||||
|
||||
ValueAnimator.ofInt(progress, newProgress).apply {
|
||||
interpolator = FastOutSlowInInterpolator()
|
||||
addUpdateListener { progress = it.animatedValue as Int }
|
||||
tag = this
|
||||
}.start()
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setTextSafe(text: Int) {
|
||||
if (text == 0) this.text = null else setText(text)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:onLongClick")
|
||||
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
|
||||
setOnLongClickListener {
|
||||
listener()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("strikeThrough")
|
||||
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
|
||||
paintFlags = if (useStrikeThrough) {
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
} else {
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("spanCount")
|
||||
fun RecyclerView.setSpanCount(count: Int) {
|
||||
when (val lama = layoutManager) {
|
||||
is GridLayoutManager -> lama.spanCount = count
|
||||
is StaggeredGridLayoutManager -> lama.spanCount = count
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("cardBackgroundColorAttr")
|
||||
fun CardView.setCardBackgroundColorAttr(attr: Int) {
|
||||
val tv = TypedValue()
|
||||
context.theme.resolveAttribute(attr, tv, true)
|
||||
setCardBackgroundColor(tv.data)
|
||||
}
|
||||
|
||||
@BindingAdapter("tint")
|
||||
fun ImageView.setTint(color: Int) {
|
||||
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color))
|
||||
}
|
||||
|
||||
@BindingAdapter("tintAttr")
|
||||
fun ImageView.setTintAttr(attr: Int) {
|
||||
val tv = TypedValue()
|
||||
context.theme.resolveAttribute(attr, tv, true)
|
||||
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(tv.data))
|
||||
}
|
||||
|
||||
@BindingAdapter("textColorAttr")
|
||||
fun TextView.setTextColorAttr(attr: Int) {
|
||||
val tv = TypedValue()
|
||||
context.theme.resolveAttribute(attr, tv, true)
|
||||
setTextColor(tv.data)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setText(text: TextHolder) {
|
||||
this.text = text.getText(context.resources)
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
|
||||
/**
|
||||
* Modified from https://github.com/skoumalcz/teanity/blob/1.2/core/src/main/java/com/skoumal/teanity/observable/Notifyable.kt
|
||||
*
|
||||
* Interface that allows user to be observed via DataBinding or manually by assigning listeners.
|
||||
*
|
||||
* @see [androidx.databinding.Observable]
|
||||
* */
|
||||
interface ObservableHost : Observable {
|
||||
|
||||
var callbacks: PropertyChangeRegistry?
|
||||
|
||||
/**
|
||||
* Notifies all observers that something has changed. By default implementation this method is
|
||||
* synchronous, hence observers will never be notified in undefined order. Observers might
|
||||
* choose to refresh the view completely, which is beyond the scope of this function.
|
||||
* */
|
||||
fun notifyChange() {
|
||||
synchronized(this) {
|
||||
callbacks ?: return
|
||||
}.notifyCallbacks(this, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all observers about field with [fieldId] has been changed. This will happen
|
||||
* synchronously before or after [notifyChange] has been called. It will never be called during
|
||||
* the execution of aforementioned method.
|
||||
* */
|
||||
fun notifyPropertyChanged(fieldId: Int) {
|
||||
synchronized(this) {
|
||||
callbacks ?: return
|
||||
}.notifyCallbacks(this, fieldId, null)
|
||||
}
|
||||
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
synchronized(this) {
|
||||
callbacks ?: PropertyChangeRegistry().also { callbacks = it }
|
||||
}.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
synchronized(this) {
|
||||
callbacks ?: return
|
||||
}.remove(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun ObservableHost.addOnPropertyChangedCallback(
|
||||
fieldId: Int,
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: () -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
if (fieldId == propertyId) {
|
||||
callback()
|
||||
if (removeAfterChanged)
|
||||
removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects boilerplate implementation for {@literal @}[androidx.databinding.Bindable] field setters.
|
||||
*
|
||||
* # Examples:
|
||||
* ```kotlin
|
||||
* @get:Bindable
|
||||
* var myField = defaultValue
|
||||
* set(value) = set(value, field, { field = it }, BR.myField) {
|
||||
* doSomething(it)
|
||||
* }
|
||||
* ```
|
||||
* */
|
||||
|
||||
inline fun <reified T> ObservableHost.set(
|
||||
new: T, old: T, setter: (T) -> Unit, fieldId: Int, afterChanged: (T) -> Unit = {}) {
|
||||
if (old != new) {
|
||||
setter(new)
|
||||
notifyPropertyChanged(fieldId)
|
||||
afterChanged(new)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> ObservableHost.set(
|
||||
new: T, old: T, setter: (T) -> Unit, vararg fieldIds: Int, afterChanged: (T) -> Unit = {}) {
|
||||
if (old != new) {
|
||||
setter(new)
|
||||
fieldIds.forEach { notifyPropertyChanged(it) }
|
||||
afterChanged(new)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class RvItem {
|
||||
abstract val layoutRes: Int
|
||||
}
|
||||
|
||||
interface ItemWrapper<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ViewAwareItem {
|
||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
interface DiffItem<T : Any> {
|
||||
|
||||
fun itemSameAs(other: T): Boolean {
|
||||
if (this === other) return true
|
||||
return when (this) {
|
||||
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||
else -> this == other
|
||||
}
|
||||
}
|
||||
|
||||
fun contentSameAs(other: T) = true
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
|
||||
class RvItemAdapter<T: RvItem>(
|
||||
val items: List<T>,
|
||||
val extraBindings: SparseArray<*>?
|
||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||
|
||||
private var lifecycleOwner: LifecycleOwner? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
|
||||
|
||||
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = rv.findViewTreeLifecycleOwner()
|
||||
recyclerView = rv
|
||||
if (items is ObservableList)
|
||||
items.addOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = null
|
||||
recyclerView = null
|
||||
if (items is ObservableList)
|
||||
items.removeOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
|
||||
val inflator = LayoutInflater.from(parent.context)
|
||||
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
extraBindings?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
|
||||
}
|
||||
}
|
||||
holder.binding.lifecycleOwner = lifecycleOwner
|
||||
holder.binding.executePendingBindings()
|
||||
recyclerView?.let {
|
||||
if (item is ViewAwareItem)
|
||||
item.onBind(holder.binding, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun getItemViewType(position: Int) = items[position].layoutRes
|
||||
|
||||
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeChanged(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeInserted(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>?,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
for (i in 0 until itemCount) {
|
||||
notifyItemMoved(fromPosition + i, toPosition + i)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeRemoved(positionStart, itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
|
||||
|
||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||
if (items != null) {
|
||||
val rva = (adapter as? RvItemAdapter<*>)
|
||||
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||
adapter = RvItemAdapter(items, extraBindings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import java.io.IOException
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@@ -21,19 +30,40 @@ abstract class MarkDownDialog : DialogBuilder {
|
||||
|
||||
@CallSuper
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
with(dialog) {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
||||
setView(view)
|
||||
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||
ServiceLocator.markwon.setMarkdown(tv, text)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
tv.setText(CoreR.string.download_file_error)
|
||||
}
|
||||
}
|
||||
dialog.setView {
|
||||
MarkdownContent(::getMarkdownText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkdownContent(getMarkdownText: suspend () -> String) {
|
||||
var mdText by remember { mutableStateOf<String?>(null) }
|
||||
var error by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
mdText = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
error = true
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
error -> Text(stringResource(CoreR.string.download_file_error))
|
||||
mdText != null -> {
|
||||
val text = mdText!!
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
TextView(context).apply {
|
||||
ServiceLocator.markwon.setMarkdown(this, text)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class UninstallDialog : DialogBuilder {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun restore(activity: UIActivity<*>) {
|
||||
private fun restore(activity: UIActivity) {
|
||||
val dialog = ProgressDialog(activity).apply {
|
||||
setMessage(activity.getString(R.string.restore_img_msg))
|
||||
show()
|
||||
|
||||
@@ -19,24 +19,24 @@ class PermissionEvent(
|
||||
private val callback: (Boolean) -> Unit
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) =
|
||||
override fun invoke(activity: UIActivity) =
|
||||
activity.withPermission(permission, callback)
|
||||
}
|
||||
|
||||
class BackPressEvent : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
class DieEvent : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
class RecreateEvent : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.relaunch()
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class AuthEvent(
|
||||
private val callback: () -> Unit
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.withAuthentication { if (it) callback() }
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class GetContentEvent(
|
||||
private val type: String,
|
||||
private val callback: ContentResultCallback
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.getContent(type, callback)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class SnackbarEvent(
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) : this(msg.asText(), length, builder)
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class SnackbarEvent(
|
||||
class DialogEvent(
|
||||
private val builder: DialogBuilder
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
override fun invoke(activity: UIActivity) {
|
||||
MagiskDialog(activity).apply(builder::build).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -55,6 +54,7 @@ import com.topjohnwu.magisk.ui.navigation.rememberNavigator
|
||||
import com.topjohnwu.magisk.ui.theme.MagiskTheme
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.MagiskDialogHost
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -63,9 +63,8 @@ import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
|
||||
class MainActivity : UIActivity<ViewDataBinding>(), SplashScreenHost {
|
||||
class MainActivity : UIActivity(), SplashScreenHost {
|
||||
|
||||
override val layoutRes = 0
|
||||
override val viewModel by viewModel<MainViewModel>()
|
||||
override val splashController = SplashController(this)
|
||||
override val snackbarView: View
|
||||
@@ -103,6 +102,7 @@ class MainActivity : UIActivity<ViewDataBinding>(), SplashScreenHost {
|
||||
|
||||
setContent {
|
||||
MagiskTheme {
|
||||
MagiskDialogHost()
|
||||
val navigator = rememberNavigator(Route.Main)
|
||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||
HandleFlashIntent(navigator)
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.topjohnwu.magisk.arch.ViewEvent
|
||||
fun ObserveViewEvents(viewModel: BaseViewModel) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val activity = context as? UIActivity<*>
|
||||
val activity = context as? UIActivity
|
||||
|
||||
DisposableEffect(viewModel, lifecycleOwner) {
|
||||
val observer = { event: ViewEvent ->
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
@@ -23,9 +22,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class SuRequestActivity : UIActivity<ViewDataBinding>(), UntrackedActivity {
|
||||
open class SuRequestActivity : UIActivity(), UntrackedActivity {
|
||||
|
||||
override val layoutRes: Int = 0
|
||||
override val viewModel: SuRequestViewModel by viewModel()
|
||||
|
||||
override val snackbarView: View
|
||||
|
||||
@@ -1,216 +1,103 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.databinding.setAdapter
|
||||
import com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.extra.SuperDialog
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
typealias DialogButtonClickListener = (DialogInterface) -> Unit
|
||||
class MagiskDialog(val context: Activity) {
|
||||
|
||||
class MagiskDialog(
|
||||
context: Activity, theme: Int = 0
|
||||
) : AppCompatDialog(context, theme) {
|
||||
val activity: UIActivity get() = context as UIActivity
|
||||
val ownerActivity: Activity get() = context
|
||||
val showState = mutableStateOf(true)
|
||||
|
||||
private val binding: DialogMagiskBaseBinding =
|
||||
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
|
||||
private val data = Data()
|
||||
internal var title by mutableStateOf<CharSequence>("")
|
||||
internal var message by mutableStateOf<CharSequence>("")
|
||||
internal var icon by mutableStateOf<Drawable?>(null)
|
||||
internal var isCancelable by mutableStateOf(true)
|
||||
internal var customContent by mutableStateOf<(@Composable () -> Unit)?>(null)
|
||||
|
||||
val activity: UIActivity<*> get() = ownerActivity as UIActivity<*>
|
||||
|
||||
init {
|
||||
binding.setVariable(BR.data, data)
|
||||
setCancelable(true)
|
||||
setOwnerActivity(context)
|
||||
}
|
||||
|
||||
inner class Data : ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@get:Bindable
|
||||
var icon: Drawable? = null
|
||||
set(value) = set(value, field, { field = it }, BR.icon)
|
||||
|
||||
@get:Bindable
|
||||
var title: CharSequence = ""
|
||||
set(value) = set(value, field, { field = it }, BR.title)
|
||||
|
||||
@get:Bindable
|
||||
var message: CharSequence = ""
|
||||
set(value) = set(value, field, { field = it }, BR.message)
|
||||
|
||||
val buttonPositive = ButtonViewModel()
|
||||
val buttonNeutral = ButtonViewModel()
|
||||
val buttonNegative = ButtonViewModel()
|
||||
}
|
||||
|
||||
enum class ButtonType {
|
||||
POSITIVE, NEUTRAL, NEGATIVE
|
||||
}
|
||||
|
||||
interface Button {
|
||||
var icon: Int
|
||||
var text: Any
|
||||
var isEnabled: Boolean
|
||||
var doNotDismiss: Boolean
|
||||
|
||||
fun onClick(listener: DialogButtonClickListener)
|
||||
}
|
||||
|
||||
inner class ButtonViewModel : Button, ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@get:Bindable
|
||||
override var icon = 0
|
||||
set(value) = set(value, field, { field = it }, BR.icon, BR.gone)
|
||||
|
||||
@get:Bindable
|
||||
var message: String = ""
|
||||
set(value) = set(value, field, { field = it }, BR.message, BR.gone)
|
||||
|
||||
override var text: Any
|
||||
get() = message
|
||||
set(value) {
|
||||
message = when (value) {
|
||||
is Int -> context.getText(value)
|
||||
else -> value
|
||||
}.toString()
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
val gone get() = icon == 0 && message.isEmpty()
|
||||
|
||||
@get:Bindable
|
||||
override var isEnabled = true
|
||||
set(value) = set(value, field, { field = it }, BR.enabled)
|
||||
|
||||
override var doNotDismiss = false
|
||||
|
||||
private var onClickAction: DialogButtonClickListener = {}
|
||||
|
||||
override fun onClick(listener: DialogButtonClickListener) {
|
||||
onClickAction = listener
|
||||
}
|
||||
|
||||
fun clicked() {
|
||||
onClickAction(this@MagiskDialog)
|
||||
if (!doNotDismiss) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
super.setContentView(binding.root)
|
||||
|
||||
val default = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, javaClass.canonicalName)
|
||||
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, default)
|
||||
val materialShapeDrawable = MaterialShapeDrawable(context, null, androidx.appcompat.R.attr.alertDialogStyle, com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents)
|
||||
materialShapeDrawable.initializeElevationOverlay(context)
|
||||
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
|
||||
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
|
||||
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
|
||||
|
||||
val inset = context.resources.getDimensionPixelSize(com.google.android.material.R.dimen.appcompat_dialog_background_inset)
|
||||
window?.apply {
|
||||
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTitle(@StringRes titleId: Int) { data.title = context.getString(titleId) }
|
||||
|
||||
override fun setTitle(title: CharSequence?) { data.title = title ?: "" }
|
||||
val buttonPositive = ButtonState()
|
||||
val buttonNeutral = ButtonState()
|
||||
val buttonNegative = ButtonState()
|
||||
|
||||
fun setTitle(@StringRes titleId: Int) { title = context.getString(titleId) }
|
||||
fun setTitle(title: CharSequence?) { this.title = title ?: "" }
|
||||
fun setMessage(@StringRes msgId: Int, vararg args: Any) {
|
||||
data.message = context.getString(msgId, *args)
|
||||
message = context.getString(msgId, *args)
|
||||
}
|
||||
|
||||
fun setMessage(message: CharSequence) { data.message = message }
|
||||
|
||||
fun setMessage(message: CharSequence) { this.message = message }
|
||||
fun setIcon(@DrawableRes drawableRes: Int) {
|
||||
data.icon = AppCompatResources.getDrawable(context, drawableRes)
|
||||
icon = AppCompatResources.getDrawable(context, drawableRes)
|
||||
}
|
||||
|
||||
fun setIcon(drawable: Drawable) { data.icon = drawable }
|
||||
fun setIcon(drawable: Drawable) { icon = drawable }
|
||||
fun setCancelable(cancelable: Boolean) { isCancelable = cancelable }
|
||||
|
||||
fun setButton(buttonType: ButtonType, builder: Button.() -> Unit) {
|
||||
val button = when (buttonType) {
|
||||
ButtonType.POSITIVE -> data.buttonPositive
|
||||
ButtonType.NEUTRAL -> data.buttonNeutral
|
||||
ButtonType.NEGATIVE -> data.buttonNegative
|
||||
}
|
||||
button.apply(builder)
|
||||
when (buttonType) {
|
||||
ButtonType.POSITIVE -> buttonPositive
|
||||
ButtonType.NEUTRAL -> buttonNeutral
|
||||
ButtonType.NEGATIVE -> buttonNegative
|
||||
}.apply(builder)
|
||||
}
|
||||
|
||||
class DialogItem(
|
||||
override val item: CharSequence,
|
||||
val position: Int
|
||||
) : RvItem(), DiffItem<DialogItem>, ItemWrapper<CharSequence> {
|
||||
override val layoutRes = R.layout.item_list_single_line
|
||||
}
|
||||
|
||||
fun interface DialogClickListener {
|
||||
fun onClick(position: Int)
|
||||
fun setView(content: @Composable () -> Unit) {
|
||||
customContent = content
|
||||
}
|
||||
|
||||
fun setListItems(
|
||||
list: Array<out CharSequence>,
|
||||
listener: DialogClickListener
|
||||
) = setView(
|
||||
RecyclerView(context).also {
|
||||
it.isNestedScrollingEnabled = false
|
||||
it.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
|
||||
val extraBindings = bindExtra { sa ->
|
||||
sa.put(BR.listener, DialogClickListener { pos ->
|
||||
listener.onClick(pos)
|
||||
dismiss()
|
||||
})
|
||||
) {
|
||||
customContent = {
|
||||
Column {
|
||||
list.forEachIndexed { index, text ->
|
||||
Text(
|
||||
text = text.toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
listener.onClick(index)
|
||||
dismiss()
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
it.setAdapter(items, extraBindings)
|
||||
}
|
||||
)
|
||||
|
||||
fun setView(view: View) {
|
||||
binding.dialogBaseContainer.removeAllViews()
|
||||
binding.dialogBaseContainer.addView(
|
||||
view,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
fun resetButtons() {
|
||||
ButtonType.values().forEach {
|
||||
ButtonType.entries.forEach {
|
||||
setButton(it) {
|
||||
text = ""
|
||||
icon = 0
|
||||
@@ -221,12 +108,136 @@ class MagiskDialog(
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent calling setContentView
|
||||
fun show() { DialogManager.show(this) }
|
||||
|
||||
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
|
||||
override fun setContentView(layoutResID: Int) {}
|
||||
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
|
||||
override fun setContentView(view: View) {}
|
||||
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
|
||||
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {}
|
||||
fun dismiss() {
|
||||
showState.value = false
|
||||
DialogManager.dismiss(this)
|
||||
}
|
||||
|
||||
enum class ButtonType { POSITIVE, NEUTRAL, NEGATIVE }
|
||||
|
||||
interface Button {
|
||||
var icon: Int
|
||||
var text: Any
|
||||
var isEnabled: Boolean
|
||||
var doNotDismiss: Boolean
|
||||
fun onClick(listener: () -> Unit)
|
||||
}
|
||||
|
||||
inner class ButtonState : Button {
|
||||
var label by mutableStateOf("")
|
||||
var iconRes by mutableIntStateOf(0)
|
||||
override var isEnabled by mutableStateOf(true)
|
||||
override var doNotDismiss = false
|
||||
internal var onClickAction: () -> Unit = {}
|
||||
|
||||
val isVisible get() = iconRes != 0 || label.isNotEmpty()
|
||||
|
||||
override var icon: Int
|
||||
get() = iconRes
|
||||
set(value) { iconRes = value }
|
||||
|
||||
override var text: Any
|
||||
get() = label
|
||||
set(value) {
|
||||
label = when (value) {
|
||||
is Int -> context.getText(value)
|
||||
else -> value
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override fun onClick(listener: () -> Unit) {
|
||||
onClickAction = listener
|
||||
}
|
||||
|
||||
fun performClick() {
|
||||
onClickAction()
|
||||
if (!doNotDismiss) dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun interface DialogClickListener {
|
||||
fun onClick(position: Int)
|
||||
}
|
||||
}
|
||||
|
||||
object DialogManager {
|
||||
val dialogs = mutableStateListOf<MagiskDialog>()
|
||||
fun show(dialog: MagiskDialog) { dialogs.add(dialog) }
|
||||
fun dismiss(dialog: MagiskDialog) { dialogs.remove(dialog) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MagiskDialogHost() {
|
||||
for (dialog in DialogManager.dialogs.toList()) {
|
||||
MagiskDialogContent(dialog)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskDialogContent(dialog: MagiskDialog) {
|
||||
SuperDialog(
|
||||
title = dialog.title.toString(),
|
||||
show = dialog.showState,
|
||||
onDismissRequest = {
|
||||
if (dialog.isCancelable) dialog.dismiss()
|
||||
}
|
||||
) {
|
||||
dialog.icon?.let { iconDrawable ->
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(iconDrawable),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (dialog.message.isNotEmpty()) {
|
||||
Text(
|
||||
text = dialog.message.toString(),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
dialog.customContent?.invoke()
|
||||
|
||||
val hasButtons = dialog.buttonNeutral.isVisible ||
|
||||
dialog.buttonNegative.isVisible ||
|
||||
dialog.buttonPositive.isVisible
|
||||
if (hasButtons) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
if (dialog.buttonNeutral.isVisible) {
|
||||
TextButton(
|
||||
text = dialog.buttonNeutral.label,
|
||||
enabled = dialog.buttonNeutral.isEnabled,
|
||||
onClick = { dialog.buttonNeutral.performClick() }
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (dialog.buttonNegative.isVisible) {
|
||||
TextButton(
|
||||
text = dialog.buttonNegative.label,
|
||||
enabled = dialog.buttonNegative.isEnabled,
|
||||
onClick = { dialog.buttonNegative.performClick() }
|
||||
)
|
||||
}
|
||||
if (dialog.buttonPositive.isVisible) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
TextButton(
|
||||
text = dialog.buttonPositive.label,
|
||||
enabled = dialog.buttonPositive.isEnabled,
|
||||
onClick = { dialog.buttonPositive.performClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout 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">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="data"
|
||||
type="com.topjohnwu.magisk.view.MagiskDialog.Data" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:layout_width="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/dialog_base_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="16dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/dialog_base_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="16dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dialog_base_icon"
|
||||
style="@style/WidgetFoundation.Image.Big"
|
||||
gone="@{data.icon == null}"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="@dimen/l1"
|
||||
android:src="@{data.icon}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialog_base_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_delete_md2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialog_base_title"
|
||||
gone="@{data.title.length == 0}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/l1"
|
||||
android:gravity="center"
|
||||
android:text="@{data.title}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Title"
|
||||
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
|
||||
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
|
||||
tools:lines="1"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/l_50"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialog_base_space"
|
||||
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
|
||||
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialog_base_message"
|
||||
gone="@{data.message.length == 0}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@{data.message}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Body"
|
||||
tools:lines="3"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_base_container"
|
||||
gone="@{data.message.length != 0}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<Space
|
||||
android:id="@+id/dialog_base_space"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/l_50"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialog_base_buttons"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.ButtonBarLayout
|
||||
android:id="@+id/dialog_base_buttons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom|center_horizontal"
|
||||
android:layoutDirection="locale"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/dialog_base_button_2"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
gone="@{data.buttonNeutral.gone}"
|
||||
isEnabled="@{data.buttonNeutral.isEnabled}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonNeutral.isEnabled}"
|
||||
android:focusable="@{data.buttonNeutral.isEnabled}"
|
||||
android:onClick="@{() -> data.buttonNeutral.clicked()}"
|
||||
android:text="@{data.buttonNeutral.message}"
|
||||
app:icon="@{data.buttonNeutral.icon}"
|
||||
app:iconGravity="textStart"
|
||||
tools:icon="@drawable/ic_bug_md2"
|
||||
tools:text="Button 1" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/spacer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/dialog_base_button_3"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
gone="@{data.buttonNegative.gone}"
|
||||
isEnabled="@{data.buttonNegative.isEnabled}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonNegative.isEnabled}"
|
||||
android:focusable="@{data.buttonNegative.isEnabled}"
|
||||
android:onClick="@{() -> data.buttonNegative.clicked()}"
|
||||
android:text="@{data.buttonNegative.message}"
|
||||
app:icon="@{data.buttonNegative.icon}"
|
||||
tools:icon="@drawable/ic_bug_md2"
|
||||
tools:text="Button 1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/dialog_base_button_1"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
gone="@{data.buttonPositive.gone}"
|
||||
isEnabled="@{data.buttonPositive.isEnabled}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonPositive.isEnabled}"
|
||||
android:focusable="@{data.buttonPositive.isEnabled}"
|
||||
android:onClick="@{() -> data.buttonPositive.clicked()}"
|
||||
android:text="@{data.buttonPositive.message}"
|
||||
app:icon="@{data.buttonPositive.icon}"
|
||||
app:iconGravity="textStart"
|
||||
tools:icon="@drawable/ic_bug_md2"
|
||||
tools:text="Button 1" />
|
||||
|
||||
</androidx.appcompat.widget.ButtonBarLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.topjohnwu.magisk.view.MagiskDialog.DialogItem" />
|
||||
|
||||
<variable
|
||||
name="listener"
|
||||
type="com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener" />
|
||||
|
||||
</data>
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:onClick="@{() -> listener.onClick(item.position)}"
|
||||
android:paddingStart="@dimen/l1"
|
||||
android:paddingTop="@dimen/l_75"
|
||||
android:paddingEnd="@dimen/l1"
|
||||
android:paddingBottom="@dimen/l_75"
|
||||
android:text="@{item.item}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Body"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
</layout>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/md_txt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:breakStrategy="simple"
|
||||
android:hyphenationFrequency="none"
|
||||
android:paddingTop="10dp"
|
||||
android:textAppearance="@style/AppearanceFoundation.Caption"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
</ScrollView>
|
||||
@@ -3,7 +3,6 @@ kotlin = "2.3.0"
|
||||
android = "9.0.1"
|
||||
ksp = "2.3.4"
|
||||
rikka = "1.3.0"
|
||||
navigation = "2.9.7"
|
||||
libsu = "6.0.0"
|
||||
okhttp = "5.3.2"
|
||||
retrofit = "3.0.0"
|
||||
@@ -33,17 +32,10 @@ activity = { module = "androidx.activity:activity", version = "1.12.4" }
|
||||
appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" }
|
||||
core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" }
|
||||
core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" }
|
||||
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" }
|
||||
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version = "1.8.9" }
|
||||
navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||
navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version = "1.4.1" }
|
||||
recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.4.0" }
|
||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.2.0" }
|
||||
transition = { module = "androidx.transition:transition", version = "1.7.0" }
|
||||
collection-ktx = { module = "androidx.collection:collection-ktx", version = "1.5.0" }
|
||||
material = { module = "com.google.android.material:material", version = "1.13.0" }
|
||||
jdk-libs = { module = "com.android.tools:desugar_jdk_libs_nio", version = "2.1.5" }
|
||||
@@ -53,13 +45,11 @@ test-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
|
||||
test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.3.0" }
|
||||
|
||||
# topjohnwu
|
||||
indeterminate-checkbox = { module = "com.github.topjohnwu:indeterminate-checkbox", version = "1.0.7" }
|
||||
libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
|
||||
libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }
|
||||
libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" }
|
||||
|
||||
# Rikka
|
||||
rikka-recyclerview = { module = "dev.rikka.rikkax.recyclerview:recyclerview-ktx", version = "1.3.2" }
|
||||
rikka-layoutinflater = { module = "dev.rikka.rikkax.layoutinflater:layoutinflater", version.ref = "rikka" }
|
||||
rikka-insets = { module = "dev.rikka.rikkax.insets:insets", version.ref = "rikka" }
|
||||
|
||||
@@ -85,5 +75,3 @@ android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
lsparanoid = { id = "org.lsposed.lsparanoid", version = "0.6.0" }
|
||||
moshix = { id = "dev.zacsweers.moshix", version = "0.34.4" }
|
||||
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "android" }
|
||||
navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
|
||||
|
||||
Reference in New Issue
Block a user