mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-02-21 09:48:30 +00:00
Updated modules screen so it displays all the content in one recyclerview
Added "endless" scrolling support - this is done in order to display everything very swiftly and load as user needs it - for the most part we'll download only ~10 items and load the rest as scroll progresses, this accomplishes the illusion that whole list is being populated Added sections and updated repo view
This commit is contained in:
parent
19fd4dd89c
commit
f83f92d3fa
29
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
29
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@file:JvmMultifileClass
|
||||||
|
|
||||||
|
package com.topjohnwu.magisk.data.database
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
|
||||||
|
interface RepoBase {
|
||||||
|
|
||||||
|
fun getRepos(offset: Int, limit: Int = 10): List<Repo>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RepoByUpdatedDao : RepoBase {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||||
|
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RepoByNameDao : RepoBase {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||||
|
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||||
|
|
||||||
|
}
|
@ -7,5 +7,8 @@ import com.topjohnwu.magisk.model.entity.module.Repo
|
|||||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||||
abstract class RepoDatabase : RoomDatabase() {
|
abstract class RepoDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun repoDao() : RepoDao
|
abstract fun repoDao(): RepoDao
|
||||||
|
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
|
||||||
|
abstract fun repoByNameDao(): RepoByNameDao
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ val databaseModule = module {
|
|||||||
single { StringDao() }
|
single { StringDao() }
|
||||||
single { createRepoDatabase(get()) }
|
single { createRepoDatabase(get()) }
|
||||||
single { get<RepoDatabase>().repoDao() }
|
single { get<RepoDatabase>().repoDao() }
|
||||||
|
single { get<RepoDatabase>().repoByNameDao() }
|
||||||
|
single { get<RepoDatabase>().repoByUpdatedDao() }
|
||||||
single { RepoUpdater(get(), get()) }
|
single { RepoUpdater(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ val redesignModule = module {
|
|||||||
viewModel { HideViewModel(get()) }
|
viewModel { HideViewModel(get()) }
|
||||||
viewModel { HomeViewModel(get()) }
|
viewModel { HomeViewModel(get()) }
|
||||||
viewModel { LogViewModel() }
|
viewModel { LogViewModel() }
|
||||||
viewModel { ModuleViewModel() }
|
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||||
viewModel { RequestViewModel() }
|
viewModel { RequestViewModel() }
|
||||||
viewModel { SafetynetViewModel(get()) }
|
viewModel { SafetynetViewModel(get()) }
|
||||||
viewModel { SettingsViewModel() }
|
viewModel { SettingsViewModel() }
|
||||||
|
@ -5,6 +5,8 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.databinding.Observable
|
import androidx.databinding.Observable
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
@ -79,6 +81,31 @@ class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
|||||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SectionTitle(
|
||||||
|
val title: Int,
|
||||||
|
val button: Int = 0,
|
||||||
|
val icon: Int = 0
|
||||||
|
) : ComparableRvItem<SectionTitle>() {
|
||||||
|
override val layoutRes = R.layout.item_section_md2
|
||||||
|
|
||||||
|
override fun onBindingBound(binding: ViewDataBinding) {
|
||||||
|
super.onBindingBound(binding)
|
||||||
|
val params = binding.root.layoutParams as StaggeredGridLayoutManager.LayoutParams
|
||||||
|
params.isFullSpan = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun itemSameAs(other: SectionTitle): Boolean = this === other
|
||||||
|
override fun contentSameAs(other: SectionTitle): Boolean = this === other
|
||||||
|
}
|
||||||
|
|
||||||
|
class RepoItem(val item: Repo) : ComparableRvItem<RepoItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_repo_md2
|
||||||
|
|
||||||
|
override fun contentSameAs(other: RepoItem): Boolean = item == other.item
|
||||||
|
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
|
||||||
|
}
|
||||||
|
|
||||||
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
|
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
|
||||||
|
|
||||||
override val layoutRes = R.layout.item_module_md2
|
override val layoutRes = R.layout.item_module_md2
|
||||||
|
@ -186,7 +186,7 @@ val ManagerJson.isObsolete
|
|||||||
|
|
||||||
fun String.clipVersion() = substringAfter('-')
|
fun String.clipVersion() = substringAfter('-')
|
||||||
|
|
||||||
inline fun <T : ComparableRvItem<T>> itemBindingOf(
|
inline fun <T : ComparableRvItem<*>> itemBindingOf(
|
||||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
crossinline body: (ItemBinding<*>) -> Unit = {}
|
||||||
) = OnItemBind<T> { itemBinding, _, item ->
|
) = OnItemBind<T> { itemBinding, _, item ->
|
||||||
item.bind(itemBinding)
|
item.bind(itemBinding)
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.redesign.module
|
package com.topjohnwu.magisk.redesign.module
|
||||||
|
|
||||||
import android.graphics.Insets
|
import android.graphics.Insets
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||||
import com.topjohnwu.magisk.redesign.compat.CompatFragment
|
import com.topjohnwu.magisk.redesign.compat.CompatFragment
|
||||||
|
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>() {
|
class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>() {
|
||||||
@ -11,12 +15,33 @@ class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>
|
|||||||
override val layoutRes = R.layout.fragment_module_md2
|
override val layoutRes = R.layout.fragment_module_md2
|
||||||
override val viewModel by viewModel<ModuleViewModel>()
|
override val viewModel by viewModel<ModuleViewModel>()
|
||||||
|
|
||||||
|
private lateinit var listener: EndlessRecyclerScrollListener
|
||||||
|
|
||||||
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
activity.title = resources.getString(R.string.section_modules)
|
activity.title = resources.getString(R.string.section_modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setEndlessScroller()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
if (this::listener.isInitialized) {
|
||||||
|
binding.moduleRemote.removeOnScrollListener(listener)
|
||||||
|
}
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEndlessScroller() {
|
||||||
|
val lama = binding.moduleRemote.layoutManager as? StaggeredGridLayoutManager ?: return
|
||||||
|
lama.isAutoMeasureEnabled = false
|
||||||
|
|
||||||
|
listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote)
|
||||||
|
binding.moduleRemote.addOnScrollListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,93 +1,226 @@
|
|||||||
package com.topjohnwu.magisk.redesign.module
|
package com.topjohnwu.magisk.redesign.module
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.databinding.ViewDataBinding
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoByNameDao
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoByUpdatedDao
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
import com.topjohnwu.magisk.extensions.subscribeK
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.model.entity.module.Module
|
import com.topjohnwu.magisk.model.entity.module.Module
|
||||||
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleItem
|
import com.topjohnwu.magisk.model.entity.recycler.ModuleItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.RepoItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.SectionTitle
|
||||||
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
|
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
|
||||||
import com.topjohnwu.magisk.redesign.home.itemBindingOf
|
import com.topjohnwu.magisk.redesign.home.itemBindingOf
|
||||||
import com.topjohnwu.magisk.redesign.superuser.diffListOf
|
import com.topjohnwu.magisk.redesign.superuser.diffListOf
|
||||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||||
import com.topjohnwu.magisk.utils.currentLocale
|
import com.topjohnwu.magisk.utils.currentLocale
|
||||||
|
import io.reactivex.Completable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||||
|
|
||||||
class ModuleViewModel : CompatViewModel() {
|
class ModuleViewModel(
|
||||||
|
private val repoName: RepoByNameDao,
|
||||||
|
private val repoUpdated: RepoByUpdatedDao,
|
||||||
|
private val repoUpdater: RepoUpdater
|
||||||
|
) : CompatViewModel() {
|
||||||
|
|
||||||
val items = diffListOf<ModuleItem>()
|
val adapter = adapterOf<ComparableRvItem<*>>()
|
||||||
val itemsPending = diffListOf<ModuleItem>()
|
val items = diffListOf<ComparableRvItem<*>>()
|
||||||
val itemBinding = itemBindingOf<ModuleItem> {
|
val itemBinding = itemBindingOf<ComparableRvItem<*>> {
|
||||||
it.bindExtra(BR.viewModel, this)
|
it.bindExtra(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val sectionRemote = SectionTitle(R.string.module_section_remote)
|
||||||
|
private val sectionActive = SectionTitle(R.string.module_section_active)
|
||||||
|
private val sectionPending =
|
||||||
|
SectionTitle(R.string.module_section_pending, R.string.reboot, R.drawable.ic_restart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
private val itemsPending
|
||||||
|
@WorkerThread get() = items.asSequence()
|
||||||
|
.filterIsInstance<ModuleItem>()
|
||||||
|
.filter { it.isModified }
|
||||||
|
.toList()
|
||||||
|
private val itemsActive
|
||||||
|
@WorkerThread get() = items.asSequence()
|
||||||
|
.filterIsInstance<ModuleItem>()
|
||||||
|
.filter { !it.isModified }
|
||||||
|
.toList()
|
||||||
|
private val itemsRemote
|
||||||
|
@WorkerThread get() = items.filterIsInstance<RepoItem>()
|
||||||
|
|
||||||
|
private var remoteJob: Disposable? = null
|
||||||
|
private val dao
|
||||||
|
get() = when (Config.repoOrder) {
|
||||||
|
Config.Value.ORDER_DATE -> repoUpdated
|
||||||
|
Config.Value.ORDER_NAME -> repoName
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
override fun refresh() = Single.fromCallable { Module.loadModules() }
|
override fun refresh() = Single.fromCallable { Module.loadModules() }
|
||||||
.map { it.map { ModuleItem(it) } }
|
.map { it.map { ModuleItem(it) } }
|
||||||
.map { it.order() }
|
.map { it.order() }
|
||||||
.subscribeK { it.forEach { it.update() } }
|
.map {
|
||||||
|
val pending = it.getValue(ModuleState.Modified)
|
||||||
|
val active = it.getValue(ModuleState.Normal)
|
||||||
|
build(pending = pending, active = active)
|
||||||
|
}
|
||||||
|
.map { it to items.calculateDiff(it) }
|
||||||
|
.subscribeK {
|
||||||
|
items.update(it.first, it.second)
|
||||||
|
if (!items.contains(sectionRemote)) {
|
||||||
|
loadRemote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun loadRemote() {
|
||||||
|
// check for existing jobs
|
||||||
|
val size = itemsRemote.size
|
||||||
|
if (remoteJob?.isDisposed?.not() == true || size % 10 != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteJob = loadRepos(offset = size)
|
||||||
|
.map { it.map { RepoItem(it) } }
|
||||||
|
.applyViewModel(this)
|
||||||
|
.subscribeK {
|
||||||
|
if (!items.contains(sectionRemote)) {
|
||||||
|
items.add(sectionRemote)
|
||||||
|
}
|
||||||
|
items.addAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadRepos(
|
||||||
|
offset: Int = 0,
|
||||||
|
downloadRepos: Boolean = offset == 0
|
||||||
|
): Single<List<Repo>> = Single.fromCallable { dao.getRepos(offset) }.flatMap {
|
||||||
|
when {
|
||||||
|
// in case we find result empty and offset is initial we need to refresh the repos.
|
||||||
|
downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos()
|
||||||
|
.andThen(loadRepos(downloadRepos = false))
|
||||||
|
else -> Single.just(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadRepos() = Single.just(Unit)
|
||||||
|
.flatMap { repoUpdater() }
|
||||||
|
.ignoreElement()
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun List<ModuleItem>.order() = sortedBy { it.item.name.toLowerCase(currentLocale) }
|
private fun List<ModuleItem>.order() = asSequence()
|
||||||
|
.sortedBy { it.item.name.toLowerCase(currentLocale) }
|
||||||
.groupBy {
|
.groupBy {
|
||||||
when {
|
when {
|
||||||
it.isModified -> ModuleState.Modified
|
it.isModified -> ModuleState.Modified
|
||||||
else -> ModuleState.Normal
|
else -> ModuleState.Normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
|
||||||
val diff = when (it.key) {
|
|
||||||
ModuleState.Modified -> itemsPending
|
|
||||||
ModuleState.Normal -> items
|
|
||||||
}.calculateDiff(it.value)
|
|
||||||
ResultEnclosure(it.key, it.value, diff)
|
|
||||||
}
|
|
||||||
.ensureAllStates()
|
.ensureAllStates()
|
||||||
|
|
||||||
private fun List<ResultEnclosure>.ensureAllStates(): List<ResultEnclosure> {
|
private fun Map<ModuleState, List<ModuleItem>>.ensureAllStates(): Map<ModuleState, List<ModuleItem>> {
|
||||||
val me = this as? MutableList<ResultEnclosure> ?: this.toMutableList()
|
val me = this as? MutableMap<ModuleState, List<ModuleItem>> ?: this.toMutableMap()
|
||||||
ModuleState.values().forEach {
|
ModuleState.values().forEach {
|
||||||
if (me.none { rit -> it == rit.state }) {
|
if (me.none { rit -> it == rit.key }) {
|
||||||
me.add(ResultEnclosure(it, listOf(), null))
|
me[it] = listOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return me
|
return me
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToState(item: ModuleItem) {
|
// ---
|
||||||
items.removeAll { it.itemSameAs(item) }
|
|
||||||
itemsPending.removeAll { it.itemSameAs(item) }
|
|
||||||
|
|
||||||
if (item.isModified) {
|
@UiThread
|
||||||
itemsPending
|
fun moveToState(item: ModuleItem) {
|
||||||
} else {
|
items.removeAll { it.genericItemSameAs(item) }
|
||||||
items
|
|
||||||
}.apply {
|
val isPending = item.isModified
|
||||||
add(item)
|
|
||||||
sortWith(compareBy { it.item.name.toLowerCase(currentLocale) })
|
Single.fromCallable { if (isPending) itemsPending else itemsActive }
|
||||||
}
|
.map { (listOf(item) + it).toMutableList() }
|
||||||
|
.map { it.apply { sortWith(compareBy { it.item.name.toLowerCase(currentLocale) }) } }
|
||||||
|
.map {
|
||||||
|
if (isPending) build(pending = it)
|
||||||
|
else build(active = it)
|
||||||
|
}
|
||||||
|
.map { it to items.calculateDiff(it) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnSuccess { items.update(it.first, it.second) }
|
||||||
|
.ignoreElement()
|
||||||
|
.andThen(cleanup())
|
||||||
|
.subscribeK()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
private fun cleanup() = Completable
|
||||||
|
.concat(listOf(cleanPending(), cleanActive(), cleanRemote()))
|
||||||
|
|
||||||
|
private fun cleanPending() = Single.fromCallable { itemsPending }
|
||||||
|
.filter { it.isEmpty() }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnSuccess { items.remove(sectionPending) }
|
||||||
|
.ignoreElement()
|
||||||
|
|
||||||
|
private fun cleanActive() = Single.fromCallable { itemsActive }
|
||||||
|
.filter { it.isEmpty() }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnSuccess { items.remove(sectionActive) }
|
||||||
|
.ignoreElement()
|
||||||
|
|
||||||
|
private fun cleanRemote() = Single.fromCallable { itemsRemote }
|
||||||
|
.filter { it.isEmpty() }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnSuccess { items.remove(sectionRemote) }
|
||||||
|
.ignoreElement()
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
private enum class ModuleState {
|
private enum class ModuleState {
|
||||||
Modified, Normal
|
Modified, Normal
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ResultEnclosure(
|
// ---
|
||||||
val state: ModuleState,
|
|
||||||
val list: List<ModuleItem>,
|
|
||||||
val diff: DiffUtil.DiffResult?
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun ResultEnclosure.update() = when (state) {
|
/** Callable only from worker thread because of expensive list filtering */
|
||||||
ModuleState.Modified -> itemsPending
|
@WorkerThread
|
||||||
ModuleState.Normal -> items
|
private fun build(
|
||||||
}.update(list, diff)
|
pending: List<ModuleItem> = itemsPending,
|
||||||
|
active: List<ModuleItem> = itemsActive,
|
||||||
|
remote: List<RepoItem> = itemsRemote
|
||||||
|
) = pending.prependIfNotEmpty { sectionPending } +
|
||||||
|
active.prependIfNotEmpty { sectionActive } +
|
||||||
|
remote.prependIfNotEmpty { sectionRemote }
|
||||||
|
|
||||||
private fun <T> DiffObservableList<T>.update(list: List<T>, diff: DiffUtil.DiffResult?) {
|
private fun <T> List<T>.prependIfNotEmpty(item: () -> T) =
|
||||||
diff ?: let {
|
if (isNotEmpty()) listOf(item()) + this else this
|
||||||
update(list)
|
|
||||||
return
|
}
|
||||||
}
|
|
||||||
update(list, diff)
|
fun <T : ComparableRvItem<*>> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||||
|
override fun onBindBinding(
|
||||||
|
binding: ViewDataBinding,
|
||||||
|
variableId: Int,
|
||||||
|
layoutRes: Int,
|
||||||
|
position: Int,
|
||||||
|
item: T
|
||||||
|
) {
|
||||||
|
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||||
|
item.onBindingBound(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -135,7 +135,7 @@ class SuperuserViewModel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T : ComparableRvItem<T>> diffListOf(
|
inline fun <T : ComparableRvItem<*>> diffListOf(
|
||||||
vararg newItems: T
|
vararg newItems: T
|
||||||
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||||
|
@ -450,4 +450,9 @@ fun View.setRotationNotAnimated(rotation: Int) {
|
|||||||
if (animation != null) {
|
if (animation != null) {
|
||||||
this.rotation = rotation.toFloat()
|
this.rotation = rotation.toFloat()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun TextView.setTextSafe(text: Int) {
|
||||||
|
if (text == 0) this.text = null else setText(text)
|
||||||
}
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
|
|
||||||
|
class EndlessRecyclerScrollListener(
|
||||||
|
private val layoutManager: RecyclerView.LayoutManager,
|
||||||
|
private val loadMore: (page: Int, totalItemsCount: Int, view: RecyclerView?) -> Unit,
|
||||||
|
private val direction: Direction = Direction.BOTTOM,
|
||||||
|
visibleRowsThreshold: Int = VISIBLE_THRESHOLD
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
layoutManager: RecyclerView.LayoutManager,
|
||||||
|
loadMore: () -> Unit,
|
||||||
|
direction: Direction = Direction.BOTTOM,
|
||||||
|
visibleRowsThreshold: Int = VISIBLE_THRESHOLD
|
||||||
|
) : this(layoutManager, { _, _, _ -> loadMore() }, direction, visibleRowsThreshold)
|
||||||
|
|
||||||
|
enum class Direction {
|
||||||
|
TOP, BOTTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VISIBLE_THRESHOLD = 5
|
||||||
|
private const val STARTING_PAGE_INDEX = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The minimum amount of items to have above/below your current scroll position
|
||||||
|
// before loading more.
|
||||||
|
private val visibleThreshold = when (layoutManager) {
|
||||||
|
is LinearLayoutManager -> visibleRowsThreshold
|
||||||
|
is GridLayoutManager -> visibleRowsThreshold * layoutManager.spanCount
|
||||||
|
is StaggeredGridLayoutManager -> visibleRowsThreshold * layoutManager.spanCount
|
||||||
|
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current offset index of data you have loaded
|
||||||
|
private var currentPage = 0
|
||||||
|
// The total number of items in the dataset after the last load
|
||||||
|
private var previousTotalItemCount = 0
|
||||||
|
// True if we are still waiting for the last set of data to load.
|
||||||
|
private var loading = true
|
||||||
|
|
||||||
|
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||||
|
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||||
|
// but first we check if we are waiting for the previous load to finish.
|
||||||
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (dx == 0 && dy == 0) return
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
|
||||||
|
val visibleItemPosition = if (direction == Direction.BOTTOM) {
|
||||||
|
when (layoutManager) {
|
||||||
|
is StaggeredGridLayoutManager -> layoutManager.findLastVisibleItemPositions(null).max()
|
||||||
|
?: 0
|
||||||
|
is GridLayoutManager -> layoutManager.findLastVisibleItemPosition()
|
||||||
|
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
|
||||||
|
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (layoutManager) {
|
||||||
|
is StaggeredGridLayoutManager -> layoutManager.findFirstVisibleItemPositions(null).min()
|
||||||
|
?: 0
|
||||||
|
is GridLayoutManager -> layoutManager.findFirstVisibleItemPosition()
|
||||||
|
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
|
||||||
|
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the total item count is zero and the previous isn't, assume the
|
||||||
|
// list is invalidated and should be reset back to initial state
|
||||||
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
|
currentPage =
|
||||||
|
STARTING_PAGE_INDEX
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
if (totalItemCount == 0) {
|
||||||
|
loading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it’s still loading, we check to see if the dataset count has
|
||||||
|
// changed, if so we conclude it has finished loading and update the current page
|
||||||
|
// number and total item count.
|
||||||
|
if (loading && totalItemCount > previousTotalItemCount) {
|
||||||
|
loading = false
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it isn’t currently loading, we check to see if we have breached
|
||||||
|
// the visibleThreshold and need to reload more data.
|
||||||
|
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||||
|
// threshold should reflect how many total columns there are too
|
||||||
|
if (!loading && shouldLoadMoreItems(visibleItemPosition, totalItemCount)) {
|
||||||
|
currentPage++
|
||||||
|
loadMore(currentPage, totalItemCount, view)
|
||||||
|
loading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldLoadMoreItems(visibleItemPosition: Int, itemCount: Int) = when (direction) {
|
||||||
|
Direction.TOP -> visibleItemPosition < visibleThreshold
|
||||||
|
Direction.BOTTOM -> visibleItemPosition + visibleThreshold > itemCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this method whenever performing new searches
|
||||||
|
fun resetState() {
|
||||||
|
currentPage = STARTING_PAGE_INDEX
|
||||||
|
previousTotalItemCount = 0
|
||||||
|
loading = true
|
||||||
|
}
|
||||||
|
}
|
@ -15,145 +15,23 @@
|
|||||||
|
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/module_remote"
|
||||||
|
adapter="@{viewModel.adapter}"
|
||||||
|
dividerHorizontal="@{R.drawable.divider_l1}"
|
||||||
|
dividerVertical="@{R.drawable.divider_l1}"
|
||||||
|
itemBinding="@{viewModel.itemBinding}"
|
||||||
|
items="@{viewModel.items}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:fillViewport="true"
|
android:orientation="vertical"
|
||||||
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
|
android:paddingStart="@dimen/l1"
|
||||||
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
|
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
|
||||||
tools:paddingBottom="64dp"
|
android:paddingEnd="0dp"
|
||||||
tools:paddingTop="24dp">
|
android:paddingBottom="@{viewModel.insets.bottom}"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
|
||||||
<LinearLayout
|
app:spanCount="2"
|
||||||
android:layout_width="match_parent"
|
tools:listitem="@layout/item_module_md2" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:id="@+id/module_notice"
|
|
||||||
style="?styleCardNormal"
|
|
||||||
gone="@{!Config.coreOnly}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="@dimen/l1"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:cardBackgroundColor="?colorError"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:padding="@dimen/l1"
|
|
||||||
android:text="@string/module_safe_mode_message"
|
|
||||||
android:textAppearance="?appearanceTextCaptionOnPrimary"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
gone="@{viewModel.itemsPending.empty}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/l1"
|
|
||||||
android:paddingEnd="@dimen/l1">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/l1"
|
|
||||||
android:layout_marginBottom="@dimen/l1"
|
|
||||||
android:text="Applied on next boot"
|
|
||||||
android:textAppearance="?appearanceTextBodyNormal"
|
|
||||||
android:textColor="?colorPrimaryTransient"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_reboot_button"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/module_reboot_button"
|
|
||||||
style="?styleButtonText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/reboot"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
app:icon="@drawable/ic_restart"
|
|
||||||
app:iconPadding="@dimen/l_50"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
dividerHorizontal="@{R.drawable.divider_l1}"
|
|
||||||
dividerVertical="@{R.drawable.divider_l1}"
|
|
||||||
gone="@{viewModel.itemsPending.empty}"
|
|
||||||
itemBinding="@{viewModel.itemBinding}"
|
|
||||||
items="@{viewModel.itemsPending}"
|
|
||||||
nestedScrollingEnabled="@{false}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="@dimen/l1"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
|
|
||||||
app:spanCount="2"
|
|
||||||
tools:itemCount="1"
|
|
||||||
tools:listitem="@layout/item_module_md2" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
gone="@{viewModel.itemsPending.empty || viewModel.items.empty}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/l1"
|
|
||||||
android:paddingEnd="@dimen/l1"
|
|
||||||
android:text="Active"
|
|
||||||
android:textAppearance="?appearanceTextBodyNormal"
|
|
||||||
android:textColor="?colorPrimaryTransient"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_reboot_button"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
dividerHorizontal="@{R.drawable.divider_l1}"
|
|
||||||
dividerVertical="@{R.drawable.divider_l1}"
|
|
||||||
gone="@{viewModel.items.empty}"
|
|
||||||
itemBinding="@{viewModel.itemBinding}"
|
|
||||||
items="@{viewModel.items}"
|
|
||||||
nestedScrollingEnabled="@{false}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/l1"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="@dimen/l1"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_notice"
|
|
||||||
app:spanCount="2"
|
|
||||||
tools:itemCount="3"
|
|
||||||
tools:listitem="@layout/item_module_md2" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
style="?styleButtonText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginTop="@dimen/l1"
|
|
||||||
android:text="Download more"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
app:icon="@drawable/ic_download_md2" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
</layout>
|
</layout>
|
115
app/src/main/res/layout/item_repo_md2.xml
Normal file
115
app/src/main/res/layout/item_repo_md2.xml
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?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="item"
|
||||||
|
type="com.topjohnwu.magisk.model.entity.recycler.RepoItem" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="com.topjohnwu.magisk.redesign.module.ModuleViewModel" />
|
||||||
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="?styleCardVariant"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:layout_gravity="center"
|
||||||
|
tools:layout_marginBottom="@dimen/l1"
|
||||||
|
tools:layout_marginEnd="@dimen/l1">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/module_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/l1"
|
||||||
|
android:layout_marginTop="@dimen/l1"
|
||||||
|
android:layout_marginEnd="@dimen/l1"
|
||||||
|
android:text="@{item.item.name}"
|
||||||
|
android:textAppearance="?appearanceTextBodyNormal"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/module_version_author"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/l1"
|
||||||
|
android:layout_marginEnd="@dimen/l1"
|
||||||
|
android:text="@{@string/module_version_author(item.item.version ?? `?`, item.item.author ?? `?`)}"
|
||||||
|
android:textAppearance="?appearanceTextCaptionVariant"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/module_title"
|
||||||
|
tools:text="v1 by topjohnwu" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/module_description"
|
||||||
|
gone="@{item.item.description.empty}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/l1"
|
||||||
|
android:layout_marginTop="@dimen/l1"
|
||||||
|
android:layout_marginEnd="@dimen/l1"
|
||||||
|
android:text="@{item.item.description}"
|
||||||
|
android:textAppearance="?appearanceTextCaptionVariant"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/module_version_author"
|
||||||
|
tools:lines="4"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/module_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="@dimen/l1"
|
||||||
|
android:background="?colorSurfaceVariant"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/module_description" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/l1"
|
||||||
|
android:layout_marginEnd="@dimen/l1"
|
||||||
|
android:text="@{@string/updated_on(item.item.lastUpdateString)}"
|
||||||
|
android:textAppearance="?appearanceTextCaptionVariant"
|
||||||
|
android:textSize="11sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/module_download"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/module_divider"
|
||||||
|
tools:ignore="SmallSp"
|
||||||
|
tools:text="@string/updated_on" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/module_download"
|
||||||
|
style="?styleIconPrimary"
|
||||||
|
android:contentDescription="@string/download"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/module_divider"
|
||||||
|
app:srcCompat="@drawable/ic_download_md2" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
style="?styleProgressDeterminate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
tools:progress="40" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</layout>
|
||||||
|
|
48
app/src/main/res/layout/item_section_md2.xml
Normal file
48
app/src/main/res/layout/item_section_md2.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="item"
|
||||||
|
type="com.topjohnwu.magisk.model.entity.recycler.SectionTitle" />
|
||||||
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/module_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@{item.title}"
|
||||||
|
android:textAppearance="?appearanceTextBodyNormal"
|
||||||
|
android:textColor="?colorPrimaryTransient"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/module_button"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/module_button"
|
||||||
|
style="?styleButtonText"
|
||||||
|
gone="@{item.button == 0 || item.icon == 0}"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/l1"
|
||||||
|
android:text="@{item.button}"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@{item.icon}"
|
||||||
|
app:iconPadding="@dimen/l_50"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/module_title"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
@ -75,6 +75,9 @@
|
|||||||
|
|
||||||
<string name="module_safe_mode_message">You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled.</string>
|
<string name="module_safe_mode_message">You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled.</string>
|
||||||
<string name="module_version_author">%1$s by %2$s</string>
|
<string name="module_version_author">%1$s by %2$s</string>
|
||||||
|
<string name="module_section_pending">Pending changes</string>
|
||||||
|
<string name="module_section_active">Active</string>
|
||||||
|
<string name="module_section_remote">Remote</string>
|
||||||
|
|
||||||
<string name="superuser_toggle_log">Toggles logging</string>
|
<string name="superuser_toggle_log">Toggles logging</string>
|
||||||
<string name="superuser_toggle_notification">Toggles “toast” notifications</string>
|
<string name="superuser_toggle_notification">Toggles “toast” notifications</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user