Commit f83f92d3 authored by Viktor De Pasquale's avatar Viktor De Pasquale

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
parent 19fd4dd8
@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>
}
\ No newline at end of file
...@@ -7,5 +7,8 @@ import com.topjohnwu.magisk.model.entity.module.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 { ...@@ -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 { ...@@ -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 ...@@ -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>() { ...@@ -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 ...@@ -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)
......
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> ...@@ -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)
}
} }
\ No newline at end of file
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
} }
// ---
@UiThread
fun moveToState(item: ModuleItem) { fun moveToState(item: ModuleItem) {
items.removeAll { it.itemSameAs(item) } items.removeAll { it.genericItemSameAs(item) }
itemsPending.removeAll { it.itemSameAs(item) }
if (item.isModified) { val isPending = item.isModified
itemsPending
} else { Single.fromCallable { if (isPending) itemsPending else itemsActive }
items .map { (listOf(item) + it).toMutableList() }
}.apply { .map { it.apply { sortWith(compareBy { it.item.name.toLowerCase(currentLocale) }) } }
add(item) .map {
sortWith(compareBy { it.item.name.toLowerCase(currentLocale) }) 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)
}
}
\ No newline at end of file
...@@ -135,7 +135,7 @@ class SuperuserViewModel( ...@@ -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)
......
...@@ -451,3 +451,8 @@ fun View.setRotationNotAnimated(rotation: Int) { ...@@ -451,3 +451,8 @@ fun View.setRotationNotAnimated(rotation: Int) {
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)
}
\ No newline at end of file
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
}
}
\ No newline at end of file
...@@ -15,145 +15,23 @@ ...@@ -15,145 +15,23 @@
</data> </data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
tools:paddingBottom="64dp"
tools:paddingTop="24dp">
<LinearLayout
android:layout_width="match_parent"
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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_remote"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{R.drawable.divider_l1}" dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{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}" itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}" items="@{viewModel.items}"
nestedScrollingEnabled="@{false}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="@dimen/l1"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/l1" android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager" app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/module_notice"
app:spanCount="2" app:spanCount="2"
tools:itemCount="3"
tools:listitem="@layout/item_module_md2" /> 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>
\ No newline at end of file
<?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>
<?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>
\ No newline at end of file
...@@ -75,6 +75,9 @@ ...@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment