mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-25 01:37:39 +00:00
Support modules update
This commit is contained in:
parent
2997258fd0
commit
bc0c1980db
@ -43,7 +43,7 @@ sealed class Subject : Parcelable {
|
|||||||
val action: Action,
|
val action: Action,
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zip_url
|
override val url: String get() = module.zipUrl
|
||||||
override val title: String get() = module.downloadFilename
|
override val title: String get() = module.downloadFilename
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
|
@ -28,11 +28,9 @@ data class StubJson(
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ModuleJson(
|
data class ModuleJson(
|
||||||
val id: String,
|
val version: String,
|
||||||
val last_update: Long,
|
val versionCode: Int,
|
||||||
val prop_url: String,
|
val zipUrl: String,
|
||||||
val zip_url: String,
|
|
||||||
val notes_url: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.di.ServiceLocator
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.io.SuFile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class LocalModule(
|
data class LocalModule(
|
||||||
private val path: String,
|
private val path: String,
|
||||||
override var id: String = "",
|
|
||||||
override var name: String = "",
|
|
||||||
override var author: String = "",
|
|
||||||
override var version: String = "",
|
|
||||||
override var versionCode: Int = -1,
|
|
||||||
override var description: String = "",
|
|
||||||
) : Module() {
|
) : Module() {
|
||||||
|
override var id: String = ""
|
||||||
|
override var name: String = ""
|
||||||
|
override var version: String = ""
|
||||||
|
override var versionCode: Int = -1
|
||||||
|
var author: String = ""
|
||||||
|
var description: String = ""
|
||||||
|
var updateJson: String = ""
|
||||||
|
var updateInfo: OnlineModule? = null
|
||||||
|
|
||||||
private val removeFile = SuFile(path, "remove")
|
private val removeFile = SuFile(path, "remove")
|
||||||
private val disableFile = SuFile(path, "disable")
|
private val disableFile = SuFile(path, "disable")
|
||||||
@ -66,6 +71,30 @@ data class LocalModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(NumberFormatException::class)
|
||||||
|
private fun parseProps(props: List<String>) {
|
||||||
|
for (line in props) {
|
||||||
|
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||||
|
if (prop.size != 2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
val key = prop[0]
|
||||||
|
val value = prop[1]
|
||||||
|
if (key.isEmpty() || key[0] == '#')
|
||||||
|
continue
|
||||||
|
|
||||||
|
when (key) {
|
||||||
|
"id" -> id = value
|
||||||
|
"name" -> name = value
|
||||||
|
"version" -> version = value
|
||||||
|
"versionCode" -> versionCode = value.toInt()
|
||||||
|
"author" -> author = value
|
||||||
|
"description" -> description = value
|
||||||
|
"updateJson" -> updateJson = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
runCatching {
|
runCatching {
|
||||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||||
@ -81,13 +110,28 @@ data class LocalModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun load():Boolean {
|
||||||
|
if (updateJson.isEmpty()) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
val json = ServiceLocator.networkService.fetchModuleJson(updateJson)
|
||||||
|
if (json.versionCode > versionCode) {
|
||||||
|
updateInfo = OnlineModule(this, json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
SuFile(Const.MAGISK_PATH)
|
SuFile(Const.MAGISK_PATH)
|
||||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile }
|
||||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
|
@ -5,37 +5,10 @@ abstract class Module : Comparable<Module> {
|
|||||||
protected set
|
protected set
|
||||||
abstract var name: String
|
abstract var name: String
|
||||||
protected set
|
protected set
|
||||||
abstract var author: String
|
|
||||||
protected set
|
|
||||||
abstract var version: String
|
abstract var version: String
|
||||||
protected set
|
protected set
|
||||||
abstract var versionCode: Int
|
abstract var versionCode: Int
|
||||||
protected set
|
protected set
|
||||||
abstract var description: String
|
|
||||||
protected set
|
|
||||||
|
|
||||||
@Throws(NumberFormatException::class)
|
override operator fun compareTo(other: Module) = id.compareTo(other.id)
|
||||||
protected fun parseProps(props: List<String>) {
|
|
||||||
for (line in props) {
|
|
||||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
|
||||||
if (prop.size != 2)
|
|
||||||
continue
|
|
||||||
|
|
||||||
val key = prop[0]
|
|
||||||
val value = prop[1]
|
|
||||||
if (key.isEmpty() || key[0] == '#')
|
|
||||||
continue
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"id" -> id = value
|
|
||||||
"name" -> name = value
|
|
||||||
"version" -> version = value
|
|
||||||
"versionCode" -> versionCode = value.toInt()
|
|
||||||
"author" -> author = value
|
|
||||||
"description" -> description = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
|
|
||||||
}
|
}
|
||||||
|
@ -1,65 +1,26 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ktx.legalFilename
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Entity(tableName = "modules")
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class OnlineModule(
|
data class OnlineModule(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
override var name: String = "",
|
override var name: String,
|
||||||
override var author: String = "",
|
override var version: String,
|
||||||
override var version: String = "",
|
override var versionCode: Int,
|
||||||
override var versionCode: Int = -1,
|
val zipUrl: String,
|
||||||
override var description: String = "",
|
|
||||||
val last_update: Long,
|
|
||||||
val prop_url: String,
|
|
||||||
val zip_url: String,
|
|
||||||
val notes_url: String
|
|
||||||
) : Module(), Parcelable {
|
) : Module(), Parcelable {
|
||||||
|
constructor(local: LocalModule, json: ModuleJson) :
|
||||||
|
this(local.id, local.name, json.version, json.versionCode, json.zipUrl)
|
||||||
|
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
constructor(info: ModuleJson) : this(
|
|
||||||
id = info.id,
|
|
||||||
last_update = info.last_update,
|
|
||||||
prop_url = info.prop_url,
|
|
||||||
zip_url = info.zip_url,
|
|
||||||
notes_url = info.notes_url
|
|
||||||
)
|
|
||||||
|
|
||||||
val lastUpdate get() = Date(last_update)
|
|
||||||
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
|
|
||||||
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
||||||
|
|
||||||
suspend fun notes() = svc.fetchString(notes_url)
|
private fun String.legalFilename() = replace(" ", "_")
|
||||||
|
.replace("'", "").replace("\"", "")
|
||||||
@Throws(IllegalRepoException::class)
|
.replace("$", "").replace("`", "")
|
||||||
suspend fun load() {
|
.replace("*", "").replace("/", "_")
|
||||||
try {
|
.replace("#", "").replace("@", "")
|
||||||
val rawProps = svc.fetchString(prop_url)
|
.replace("\\", "_")
|
||||||
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
|
||||||
parseProps(props)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalRepoException("Repo [$id] parse error:", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versionCode < 0) {
|
|
||||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.data.network
|
package com.topjohnwu.magisk.data.network
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||||
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
@ -37,6 +38,9 @@ interface RawServices {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun fetchString(@Url url: String): String
|
suspend fun fetchString(@Url url: String): String
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchModuleJson(@Url url: String): ModuleJson
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GithubApiServices {
|
interface GithubApiServices {
|
||||||
|
@ -65,6 +65,7 @@ class NetworkService(
|
|||||||
}
|
}
|
||||||
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
||||||
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||||
|
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
|
||||||
|
|
||||||
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import com.topjohnwu.magisk.BR
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
|
||||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffRvItem
|
||||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
||||||
import com.topjohnwu.magisk.databinding.RvContainer
|
import com.topjohnwu.magisk.databinding.RvContainer
|
||||||
@ -16,62 +15,29 @@ object InstallModule : DiffRvItem<InstallModule>() {
|
|||||||
override val layoutRes = R.layout.item_module_download
|
override val layoutRes = R.layout.item_module_download
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionTitle(
|
|
||||||
val title: Int,
|
|
||||||
_button: Int = 0,
|
|
||||||
_icon: Int = 0
|
|
||||||
) : ObservableDiffRvItem<SectionTitle>() {
|
|
||||||
override val layoutRes = R.layout.item_section_md2
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var button = _button
|
|
||||||
set(value) = set(value, field, { field = it }, BR.button)
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var icon = _icon
|
|
||||||
set(value) = set(value, field, { field = it }, BR.icon)
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var hasButton = _button != 0 && _icon != 0
|
|
||||||
set(value) = set(value, field, { field = it }, BR.hasButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnlineModuleRvItem(
|
|
||||||
override val item: OnlineModule
|
|
||||||
) : ObservableDiffRvItem<OnlineModuleRvItem>(), RvContainer<OnlineModule> {
|
|
||||||
override val layoutRes: Int = R.layout.item_repo_md2
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var progress = 0
|
|
||||||
set(value) = set(value, field, { field = it }, BR.progress)
|
|
||||||
|
|
||||||
var hasUpdate = false
|
|
||||||
|
|
||||||
override fun itemSameAs(other: OnlineModuleRvItem): Boolean = item.id == other.item.id
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalModuleRvItem(
|
class LocalModuleRvItem(
|
||||||
override val item: LocalModule
|
override val item: LocalModule
|
||||||
) : ObservableDiffRvItem<LocalModuleRvItem>(), RvContainer<LocalModule> {
|
) : ObservableDiffRvItem<LocalModuleRvItem>(), RvContainer<LocalModule> {
|
||||||
|
|
||||||
override val layoutRes = R.layout.item_module_md2
|
override val layoutRes = R.layout.item_module_md2
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var online: OnlineModule? = null
|
|
||||||
set(value) = set(value, field, { field = it }, BR.online)
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isEnabled = item.enable
|
var isEnabled = item.enable
|
||||||
set(value) = set(value, field, { field = it }, BR.enabled) {
|
set(value) = set(value, field, { field = it }, BR.enabled, BR.updateReady) {
|
||||||
item.enable = value
|
item.enable = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isRemoved = item.remove
|
var isRemoved = item.remove
|
||||||
set(value) = set(value, field, { field = it }, BR.removed) {
|
set(value) = set(value, field, { field = it }, BR.removed, BR.updateReady) {
|
||||||
item.remove = value
|
item.remove = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var updateReady: Boolean
|
||||||
|
get() = item.updateInfo != null && !isRemoved && isEnabled
|
||||||
|
set(_) = notifyPropertyChanged(BR.updateReady)
|
||||||
|
|
||||||
val isSuspended =
|
val isSuspended =
|
||||||
(Info.isZygiskEnabled && item.isRiru) || (!Info.isZygiskEnabled && item.isZygisk)
|
(Info.isZygiskEnabled && item.isRiru) || (!Info.isZygiskEnabled && item.isZygisk)
|
||||||
|
|
||||||
@ -80,11 +46,9 @@ class LocalModuleRvItem(
|
|||||||
else R.string.suspend_text_zygisk.asText(R.string.zygisk.asText())
|
else R.string.suspend_text_zygisk.asText(R.string.zygisk.asText())
|
||||||
|
|
||||||
val isUpdated get() = item.updated
|
val isUpdated get() = item.updated
|
||||||
val isModified get() = isRemoved || isUpdated
|
|
||||||
|
|
||||||
fun delete(viewModel: ModuleViewModel) {
|
fun delete() {
|
||||||
isRemoved = !isRemoved
|
isRemoved = !isRemoved
|
||||||
viewModel.updateActiveState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id
|
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id
|
||||||
|
@ -1,63 +1,30 @@
|
|||||||
package com.topjohnwu.magisk.ui.module
|
package com.topjohnwu.magisk.ui.module
|
||||||
|
|
||||||
import androidx.databinding.Bindable
|
|
||||||
import androidx.databinding.ObservableArrayList
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.arch.Queryable
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.databinding.*
|
import com.topjohnwu.magisk.databinding.RvItem
|
||||||
import com.topjohnwu.magisk.events.OpenReadmeEvent
|
import com.topjohnwu.magisk.databinding.adapterOf
|
||||||
|
import com.topjohnwu.magisk.databinding.diffListOf
|
||||||
|
import com.topjohnwu.magisk.databinding.itemBindingOf
|
||||||
import com.topjohnwu.magisk.events.SelectModuleEvent
|
import com.topjohnwu.magisk.events.SelectModuleEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
|
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
|
||||||
import com.topjohnwu.magisk.ktx.addOnListChangedCallback
|
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
|
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
|
||||||
|
|
||||||
class ModuleViewModel : BaseViewModel(), Queryable {
|
class ModuleViewModel : BaseViewModel() {
|
||||||
|
|
||||||
val bottomBarBarrierIds =
|
val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
|
||||||
intArrayOf(R.id.module_info, R.id.module_remove)
|
|
||||||
|
|
||||||
override val queryDelay = 1000L
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var isRemoteLoading = false
|
|
||||||
set(value) = set(value, field, { field = it }, BR.remoteLoading)
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var query = ""
|
|
||||||
set(value) = set(value, field, { field = it }, BR.query) {
|
|
||||||
submitQuery()
|
|
||||||
// Yes we do lie about the search being loaded
|
|
||||||
searchLoading = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var searchLoading = false
|
|
||||||
set(value) = set(value, field, { field = it }, BR.searchLoading)
|
|
||||||
|
|
||||||
val itemsSearch = diffListOf<AnyDiffRvItem>()
|
|
||||||
val itemSearchBinding = itemBindingOf<AnyDiffRvItem> {
|
|
||||||
it.bindExtra(BR.viewModel, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val installSectionList = ObservableArrayList<RvItem>()
|
|
||||||
private val itemsInstalled = diffListOf<LocalModuleRvItem>()
|
private val itemsInstalled = diffListOf<LocalModuleRvItem>()
|
||||||
private val sectionInstalled = SectionTitle(
|
|
||||||
R.string.module_installed,
|
|
||||||
R.string.reboot,
|
|
||||||
R.drawable.ic_restart
|
|
||||||
).also { it.hasButton = false }
|
|
||||||
|
|
||||||
val adapter = adapterOf<RvItem>()
|
val adapter = adapterOf<RvItem>()
|
||||||
val items = MergeObservableList<RvItem>()
|
val items = MergeObservableList<RvItem>()
|
||||||
@ -65,34 +32,19 @@ class ModuleViewModel : BaseViewModel(), Queryable {
|
|||||||
it.bindExtra(BR.viewModel, this)
|
it.bindExtra(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
itemsInstalled.addOnListChangedCallback(
|
|
||||||
onItemRangeInserted = { _, _, _ ->
|
|
||||||
if (installSectionList.isEmpty())
|
|
||||||
installSectionList.add(sectionInstalled)
|
|
||||||
},
|
|
||||||
onItemRangeRemoved = { list, _, _ ->
|
|
||||||
if (list.isEmpty())
|
|
||||||
installSectionList.clear()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Info.env.isActive) {
|
if (Info.env.isActive) {
|
||||||
items.insertItem(InstallModule)
|
items.insertItem(InstallModule)
|
||||||
.insertList(installSectionList)
|
|
||||||
.insertList(itemsInstalled)
|
.insertList(itemsInstalled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
override fun refresh(): Job {
|
override fun refresh(): Job {
|
||||||
return viewModelScope.launch {
|
return viewModelScope.launch {
|
||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
loadInstalled()
|
loadInstalled()
|
||||||
state = State.LOADED
|
state = State.LOADED
|
||||||
|
loadUpdateInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,62 +56,21 @@ class ModuleViewModel : BaseViewModel(), Queryable {
|
|||||||
itemsInstalled.update(installed, diff)
|
itemsInstalled.update(installed, diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forceRefresh() {
|
private suspend fun loadUpdateInfo() {
|
||||||
itemsInstalled.clear()
|
withContext(Dispatchers.IO) {
|
||||||
refresh()
|
itemsInstalled.forEach {
|
||||||
submitQuery()
|
it.updateReady = it.item.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
fun downloadPressed(item: OnlineModule?) =
|
||||||
|
if (item != null && isConnected.get()) {
|
||||||
private suspend fun queryInternal(query: String): List<AnyDiffRvItem> {
|
withExternalRW { ModuleInstallDialog(item).publish() }
|
||||||
return if (query.isBlank()) {
|
|
||||||
itemsSearch.clear()
|
|
||||||
listOf()
|
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Default) {
|
SnackbarEvent(R.string.no_connection).publish()
|
||||||
itemsInstalled.filter {
|
|
||||||
it.item.id.contains(query, true)
|
|
||||||
|| it.item.name.contains(query, true)
|
|
||||||
|| it.item.description.contains(query, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun query() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val searched = queryInternal(query)
|
|
||||||
val diff = withContext(Dispatchers.Default) {
|
|
||||||
itemsSearch.calculateDiff(searched)
|
|
||||||
}
|
|
||||||
searchLoading = false
|
|
||||||
itemsSearch.update(searched, diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
fun updateActiveState() {
|
|
||||||
sectionInstalled.hasButton = itemsInstalled.any { it.isModified }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sectionPressed(item: SectionTitle) = when (item) {
|
|
||||||
sectionInstalled -> reboot()
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following methods are not used, but kept for future integration
|
|
||||||
|
|
||||||
fun downloadPressed(item: OnlineModule) =
|
|
||||||
if (isConnected.get()) withExternalRW { ModuleInstallDialog(item).publish() }
|
|
||||||
else { SnackbarEvent(R.string.no_connection).publish() }
|
|
||||||
|
|
||||||
fun installPressed() = withExternalRW { SelectModuleEvent().publish() }
|
fun installPressed() = withExternalRW { SelectModuleEvent().publish() }
|
||||||
|
|
||||||
fun infoPressed(item: OnlineModule) =
|
|
||||||
if (isConnected.get()) OpenReadmeEvent(item).publish()
|
|
||||||
else SnackbarEvent(R.string.no_connection).publish()
|
|
||||||
|
|
||||||
fun infoPressed(item: LocalModuleRvItem) { infoPressed(item.online ?: return) }
|
|
||||||
}
|
}
|
||||||
|
@ -17,21 +17,10 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!--
|
|
||||||
The list is reverted to use LinearLayoutManager only. The issue of random crashes lies in
|
|
||||||
the way StaggeredGridLayoutManager invalidates view in LazySpanLookup. Since we're adding
|
|
||||||
items in between full-span items the array is not yet invalidated and consecutively crashes
|
|
||||||
due to index of -end- being out of bounds of the current array.
|
|
||||||
|
|
||||||
If you'd like to use StaggeredGridLayoutManager, do so without adding single span items in
|
|
||||||
between of full-span items.
|
|
||||||
https://issuetracker.google.com/issues/37034096
|
|
||||||
-->
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/module_list"
|
android:id="@+id/module_list"
|
||||||
adapter="@{viewModel.adapter}"
|
adapter="@{viewModel.adapter}"
|
||||||
gone="@{viewModel.loading && viewModel.items.empty}"
|
gone="@{viewModel.loading}"
|
||||||
itemBinding="@{viewModel.itemBinding}"
|
itemBinding="@{viewModel.itemBinding}"
|
||||||
items="@{viewModel.items}"
|
items="@{viewModel.items}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -39,46 +28,13 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingTop="@dimen/internal_action_bar_size"
|
android:paddingTop="@dimen/internal_action_bar_size"
|
||||||
android:paddingBottom="56dp"
|
android:paddingBottom="@dimen/internal_action_bar_size"
|
||||||
app:fitsSystemWindowsInsets="top|bottom"
|
app:fitsSystemWindowsInsets="top|bottom"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_module_md2" />
|
tools:listitem="@layout/item_module_md2" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/module_filter_toggle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_marginStart="@dimen/l1"
|
|
||||||
android:layout_marginEnd="@dimen/l1"
|
|
||||||
android:layout_marginBottom="72dp"
|
|
||||||
app:backgroundTint="?colorSurfaceSurfaceVariant"
|
|
||||||
app:layout_fitsSystemWindowsInsets="bottom"
|
|
||||||
app:srcCompat="@drawable/ic_search_md2"
|
|
||||||
app:tint="?colorPrimary"
|
|
||||||
tools:layout_marginBottom="64dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.circularreveal.cardview.CircularRevealCardView
|
|
||||||
android:id="@+id/module_filter"
|
|
||||||
style="@style/WidgetFoundation.Card"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:cardBackgroundColor="?colorSurface"
|
|
||||||
app:cardCornerRadius="0dp">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/module_filter_include"
|
|
||||||
layout="@layout/include_module_filter"
|
|
||||||
viewModel="@{viewModel}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</com.google.android.material.circularreveal.cardview.CircularRevealCardView>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
goneUnless="@{viewModel.loading && viewModel.items.empty}"
|
goneUnless="@{viewModel.loading}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@ -98,28 +54,6 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
goneUnless="@{viewModel.remoteLoading}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_gravity="bottom">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
tools:ignore="UselessParent">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,135 +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>
|
|
||||||
|
|
||||||
<import type="com.topjohnwu.magisk.core.Config" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="viewModel"
|
|
||||||
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
|
|
||||||
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/l1"
|
|
||||||
app:fitsSystemWindowsInsets="bottom"
|
|
||||||
tools:layout_gravity="bottom"
|
|
||||||
tools:paddingBottom="64dp">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/module_filter_list"
|
|
||||||
itemBinding="@{viewModel.itemSearchBinding}"
|
|
||||||
items="@{viewModel.itemsSearch}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginBottom="@dimen/l1"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="@dimen/internal_action_bar_size"
|
|
||||||
app:fitsSystemWindowsInsets="top"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_constrainedHeight="true"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/module_filter_title_search"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:reverseLayout="false"
|
|
||||||
app:spanCount="2"
|
|
||||||
tools:listitem="@layout/item_repo_md2" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/l_50"
|
|
||||||
app:srcCompat="@drawable/bg_shadow"
|
|
||||||
app:tint="?colorSurfaceVariant"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/module_filter_list" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/module_filter_title_search"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/l1"
|
|
||||||
android:layout_marginEnd="@dimen/l1"
|
|
||||||
android:layout_marginBottom="@dimen/l1"
|
|
||||||
android:text="@string/hide_search"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:textAppearance="@style/AppearanceFoundation.Caption"
|
|
||||||
android:textColor="?colorPrimary"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/module_filter_search" />
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:id="@+id/module_filter_search"
|
|
||||||
style="@style/WidgetFoundation.Card"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/l1"
|
|
||||||
android:layout_marginEnd="@dimen/l1"
|
|
||||||
android:layout_marginBottom="@dimen/l_50"
|
|
||||||
app:cardCornerRadius="18dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_filter_done"
|
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
style="@style/WidgetFoundation.Icon"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_gravity="center_vertical|start"
|
|
||||||
android:padding="6dp"
|
|
||||||
app:srcCompat="@drawable/ic_search_md2"
|
|
||||||
app:tint="?colorDisabled" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/module_filter_search_field"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="48dp"
|
|
||||||
android:background="@null"
|
|
||||||
android:hint="@string/hide_filter_hint"
|
|
||||||
android:inputType="textUri"
|
|
||||||
android:minHeight="36dp"
|
|
||||||
android:nextFocusRight="@id/module_filter_done"
|
|
||||||
android:paddingStart="0dp"
|
|
||||||
android:paddingEnd="@dimen/l1"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@={viewModel.query}"
|
|
||||||
android:textAppearance="@style/AppearanceFoundation.Body"
|
|
||||||
android:textColor="@color/color_text_transient"
|
|
||||||
android:textColorHint="?colorOnSurfaceVariant" />
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/module_filter_done"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/l1"
|
|
||||||
android:nextFocusLeft="@id/module_filter_search_field"
|
|
||||||
app:backgroundTint="?colorPrimary"
|
|
||||||
app:elevation="0dp"
|
|
||||||
app:fabSize="mini"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/module_filter_search"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/module_filter_search"
|
|
||||||
app:srcCompat="@drawable/ic_check_md2"
|
|
||||||
app:tint="?colorOnPrimary" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
style="@style/WidgetFoundation.ProgressBar.Indeterminate.Circular"
|
|
||||||
goneUnless="@{viewModel.searchLoading}"
|
|
||||||
android:layout_width="56dp"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/module_filter_done"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/module_filter_done"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/module_filter_done"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/module_filter_done" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
@ -18,6 +18,7 @@
|
|||||||
style="@style/WidgetFoundation.Button.Outlined"
|
style="@style/WidgetFoundation.Button.Outlined"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="@dimen/l_50"
|
||||||
android:onClick="@{() -> viewModel.installPressed()}"
|
android:onClick="@{() -> viewModel.installPressed()}"
|
||||||
android:text="@string/module_action_install_external"
|
android:text="@string/module_action_install_external"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
|
@ -98,7 +98,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="@+id/module_version_author"
|
app:layout_constraintBottom_toBottomOf="@+id/module_version_author"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="1"
|
app:layout_constraintHorizontal_bias="1"
|
||||||
app:layout_constraintStart_toEndOf="@+id/module_info"
|
app:layout_constraintStart_toEndOf="@+id/module_update"
|
||||||
app:layout_constraintTop_toTopOf="@+id/module_title" />
|
app:layout_constraintTop_toTopOf="@+id/module_title" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -111,6 +111,7 @@
|
|||||||
android:layout_marginTop="@dimen/l1"
|
android:layout_marginTop="@dimen/l1"
|
||||||
android:layout_marginEnd="@dimen/l1"
|
android:layout_marginEnd="@dimen/l1"
|
||||||
android:duplicateParentState="true"
|
android:duplicateParentState="true"
|
||||||
|
android:maxLines="5"
|
||||||
android:text="@{item.item.description}"
|
android:text="@{item.item.description}"
|
||||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
||||||
android:textColor="?android:textColorSecondary"
|
android:textColor="?android:textColorSecondary"
|
||||||
@ -126,30 +127,34 @@
|
|||||||
android:background="?colorSurfaceSurfaceVariant"
|
android:background="?colorSurfaceSurfaceVariant"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_description" />
|
app:layout_constraintTop_toBottomOf="@+id/module_description" />
|
||||||
|
|
||||||
<ImageView
|
<Button
|
||||||
android:id="@+id/module_info"
|
android:id="@+id/module_update"
|
||||||
style="@style/WidgetFoundation.Icon"
|
style="@style/WidgetFoundation.Button.Text"
|
||||||
gone="@{item.online == null}"
|
gone="@{item.item.updateJson.length == 0}"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:alpha=".5"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
android:enabled="@{item.updateReady}"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:onClick="@{() -> viewModel.infoPressed(item)}"
|
android:onClick="@{() -> viewModel.downloadPressed(item.item.updateInfo)}"
|
||||||
android:paddingEnd="@dimen/l_50"
|
android:text="@string/update"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@drawable/ic_update_md2"
|
||||||
|
app:iconGravity="textEnd"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/module_remove"
|
app:layout_constraintBottom_toBottomOf="@+id/module_remove"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_remove"
|
app:layout_constraintEnd_toStartOf="@+id/module_remove"
|
||||||
app:layout_constraintTop_toTopOf="@+id/module_remove"
|
app:layout_constraintTop_toTopOf="@+id/module_remove"
|
||||||
app:srcCompat="@drawable/ic_info" />
|
app:srcCompat="@drawable/ic_download_md2" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/module_remove"
|
android:id="@+id/module_remove"
|
||||||
style="@style/WidgetFoundation.Button.Text"
|
style="@style/WidgetFoundation.Button.Text.Secondary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:enabled="@{!item.updated}"
|
android:enabled="@{!item.updated}"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:onClick="@{() -> item.delete(viewModel)}"
|
android:onClick="@{() -> item.delete()}"
|
||||||
android:text="@{item.removed ? @string/module_state_restore : @string/module_state_remove}"
|
android:text="@{item.removed ? @string/module_state_restore : @string/module_state_remove}"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
app:icon="@{item.removed ? @drawable/ic_restart : @drawable/ic_delete_md2}"
|
app:icon="@{item.removed ? @drawable/ic_restart : @drawable/ic_delete_md2}"
|
||||||
@ -165,7 +170,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="start"
|
app:barrierDirection="start"
|
||||||
app:referencedIds="@{viewModel.bottomBarBarrierIds}"
|
app:referencedIds="@{viewModel.bottomBarBarrierIds}"
|
||||||
tools:constraint_referenced_ids="module_info,module_remove" />
|
tools:constraint_referenced_ids="module_update,module_remove" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/module_suspend_text"
|
android:id="@+id/module_suspend_text"
|
||||||
|
@ -1,155 +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>
|
|
||||||
|
|
||||||
<import type="com.topjohnwu.magisk.R" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="item"
|
|
||||||
type="com.topjohnwu.magisk.ui.module.OnlineModuleRvItem" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="viewModel"
|
|
||||||
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
|
|
||||||
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:id="@+id/module_card"
|
|
||||||
style="@style/WidgetFoundation.Card"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:nextFocusRight="@id/module_info"
|
|
||||||
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">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
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="@style/AppearanceFoundation.Body"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="@tools:sample/lorem" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/module_version_author"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@{@string/module_version_author(item.item.version ?? `?`, item.item.author ?? `?`)}"
|
|
||||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/module_title"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/module_title"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_title"
|
|
||||||
tools:text="v1 by topjohnwu" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
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="@style/AppearanceFoundation.Caption.Variant"
|
|
||||||
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="?colorSurfaceSurfaceVariant"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_description" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/l1"
|
|
||||||
android:text="@{item.item.lastUpdateString}"
|
|
||||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
|
||||||
android:textSize="11sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_info"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_divider"
|
|
||||||
tools:ignore="SmallSp"
|
|
||||||
tools:text="@tools:sample/date/ddmmyy" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/module_info"
|
|
||||||
style="@style/WidgetFoundation.Icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:alpha=".5"
|
|
||||||
android:nextFocusLeft="@id/module_card"
|
|
||||||
android:onClick="@{() -> viewModel.infoPressed(item.item)}"
|
|
||||||
android:paddingEnd="@dimen/l_50"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/module_download"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/module_download"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/module_download"
|
|
||||||
app:srcCompat="@drawable/ic_info" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/module_download"
|
|
||||||
style="@style/WidgetFoundation.Icon.Primary"
|
|
||||||
isEnabled="@{!(item.progress == -100 || (item.progress > 0 && item.progress < 100))}"
|
|
||||||
srcCompat="@{item.hasUpdate ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:contentDescription="@string/download"
|
|
||||||
android:nextFocusLeft="@id/module_info"
|
|
||||||
android:onClick="@{() -> viewModel.downloadPressed(item.item)}"
|
|
||||||
android:paddingStart="@dimen/l_50"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/module_divider"
|
|
||||||
tools:srcCompat="@drawable/ic_download_md2" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
style="@style/WidgetFoundation.ProgressBar"
|
|
||||||
goneUnless="@{item.progress > 0 && item.progress < 100}"
|
|
||||||
progressAnimated="@{item.progress}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
tools:progress="40" />
|
|
||||||
|
|
||||||
<androidx.core.widget.ContentLoadingProgressBar
|
|
||||||
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
|
|
||||||
goneUnless="@{item.progress == -100}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginTop="-4dp"
|
|
||||||
android:layout_marginBottom="-5dp"
|
|
||||||
tools:progress="40" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
</layout>
|
|
@ -1,50 +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="item"
|
|
||||||
type="com.topjohnwu.magisk.ui.module.SectionTitle" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="viewModel"
|
|
||||||
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
|
|
||||||
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/module_title"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@{item.title}"
|
|
||||||
android:textAppearance="@style/AppearanceFoundation.Large"
|
|
||||||
android:textColor="@color/color_primary_transient"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="Installed" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/module_button"
|
|
||||||
style="@style/WidgetFoundation.Button.Text.Secondary"
|
|
||||||
invisible="@{!item.hasButton}"
|
|
||||||
android:onClick="@{() -> viewModel.sectionPressed(item)}"
|
|
||||||
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}"
|
|
||||||
tools:text="Reboot" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
Loading…
x
Reference in New Issue
Block a user