Skip to content

Commit 972530b

Browse files
committed
feature: compose param browser list
1 parent 8f1dddc commit 972530b

23 files changed

Lines changed: 334 additions & 457 deletions

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<activity
2525
android:name=".ui.params.edit.EditKernelParamActivity"
2626
android:label="@string/edit_params"
27+
android:launchMode="singleTask"
2728
android:theme="@style/AppTheme" />
2829
<activity
2930
android:name=".ui.main.MainActivity"

app/src/main/kotlin/com/androidvip/sysctlgui/di/PresentationModule.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import com.androidvip.sysctlgui.ui.params.browse.BrowseParamsViewModel
66
import com.androidvip.sysctlgui.ui.params.list.ListParamsViewModel
77
import com.androidvip.sysctlgui.ui.params.user.UserParamsViewModel
88
import com.androidvip.sysctlgui.widgets.FavoriteWidgetParamUpdater
9-
import kotlinx.coroutines.Dispatchers
109
import org.koin.android.ext.koin.androidContext
1110
import org.koin.androidx.viewmodel.dsl.viewModel
1211
import org.koin.androidx.viewmodel.dsl.viewModelOf
1312
import org.koin.dsl.module
1413

1514
internal val presentationModules = module {
16-
viewModel { BrowseParamsViewModel(get(), Dispatchers.IO, get()) }
15+
viewModel { BrowseParamsViewModel(get(), get()) }
1716
viewModelOf(::ListParamsViewModel)
1817
viewModelOf(::UserParamsViewModel)
1918
viewModelOf(::MainViewModel)

app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseSearchFragment.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import android.os.Bundle
44
import android.view.Menu
55
import android.view.MenuInflater
66
import android.widget.SearchView
7-
import androidx.viewbinding.ViewBinding
7+
import androidx.fragment.app.Fragment
88
import com.androidvip.sysctlgui.R
99

10-
abstract class BaseSearchFragment<Binding : ViewBinding>(
11-
private val inflate: Inflate<Binding>
12-
) : BaseViewBindingFragment<Binding>(inflate::invoke) {
10+
abstract class BaseSearchFragment : Fragment() {
1311
protected var searchExpression: String = ""
1412
private var searchView: SearchView? = null
1513

@@ -43,7 +41,8 @@ abstract class BaseSearchFragment<Binding : ViewBinding>(
4341
this@BaseSearchFragment.onQueryTextChanged()
4442
return true
4543
}
46-
})
44+
}
45+
)
4746

4847
// expand and show keyboard
4948
isIconifiedByDefault = false
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.androidvip.sysctlgui.ui.params
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.outlined.Warning
13+
import androidx.compose.material3.Card
14+
import androidx.compose.material3.CardDefaults
15+
import androidx.compose.material3.Icon
16+
import androidx.compose.material3.MaterialTheme
17+
import androidx.compose.material3.Text
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.res.stringResource
23+
import androidx.compose.ui.text.font.FontWeight
24+
import androidx.compose.ui.tooling.preview.Preview
25+
import androidx.compose.ui.unit.dp
26+
import com.androidvip.sysctlgui.R
27+
28+
@Composable
29+
fun EmptyParamsWarning() {
30+
Box(modifier = Modifier.fillMaxSize()) {
31+
Card(
32+
modifier = Modifier
33+
.fillMaxWidth()
34+
.padding(24.dp),
35+
colors = CardDefaults.cardColors(
36+
containerColor = MaterialTheme.colorScheme.errorContainer
37+
)
38+
) {
39+
Row(
40+
modifier = Modifier.padding(24.dp),
41+
horizontalArrangement = Arrangement.spacedBy(
42+
16.dp,
43+
Alignment.CenterHorizontally
44+
)
45+
) {
46+
Icon(
47+
imageVector = Icons.Outlined.Warning,
48+
contentDescription = stringResource(android.R.string.dialog_alert_title),
49+
tint = MaterialTheme.colorScheme.onErrorContainer
50+
)
51+
Column {
52+
Text(
53+
text = stringResource(id = R.string.error),
54+
style = MaterialTheme.typography.bodyLarge.copy(
55+
fontWeight = FontWeight.Medium
56+
),
57+
color = MaterialTheme.colorScheme.onErrorContainer
58+
)
59+
Text(
60+
text = stringResource(id = R.string.no_parameters_found),
61+
style = MaterialTheme.typography.bodyMedium,
62+
color = MaterialTheme.colorScheme.onErrorContainer
63+
)
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
@Composable
71+
@Preview
72+
private fun EmptyParamsWarningPreview() {
73+
Box(modifier = Modifier.background(Color.White)) {
74+
EmptyParamsWarning()
75+
}
76+
}
Lines changed: 64 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,143 @@
11
package com.androidvip.sysctlgui.ui.params.browse
22

3-
import android.app.Activity
4-
import android.view.View
5-
import androidx.core.app.ActivityOptionsCompat
6-
import androidx.lifecycle.LiveData
7-
import androidx.lifecycle.MutableLiveData
8-
import androidx.lifecycle.ViewModel
93
import androidx.lifecycle.viewModelScope
104
import com.androidvip.sysctlgui.R
115
import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
12-
import com.androidvip.sysctlgui.data.models.KernelParam
136
import com.androidvip.sysctlgui.domain.repository.AppPrefs
147
import com.androidvip.sysctlgui.domain.usecase.GetParamsFromFilesUseCase
15-
import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
8+
import com.androidvip.sysctlgui.utils.BaseViewModel
169
import com.androidvip.sysctlgui.utils.Consts
17-
import com.hadilq.liveevent.LiveEvent
18-
import com.hadilq.liveevent.LiveEventConfig
1910
import kotlinx.coroutines.CoroutineDispatcher
2011
import kotlinx.coroutines.Dispatchers
2112
import kotlinx.coroutines.launch
2213
import kotlinx.coroutines.withContext
2314
import java.io.File
24-
import androidx.core.util.Pair as PairUtil
2515

2616
class BrowseParamsViewModel(
2717
private val getParamsFromFilesUseCase: GetParamsFromFilesUseCase,
28-
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
29-
appPrefs: AppPrefs
30-
) : ViewModel() {
31-
private val _viewState = MutableLiveData<ParamBrowserViewState>()
32-
val viewState: LiveData<ParamBrowserViewState> = _viewState
33-
val viewEffect = LiveEvent<ParamBrowserViewEffect>(config = LiveEventConfig.PreferFirstObserver)
34-
35-
var listFoldersFirst = true
18+
appPrefs: AppPrefs,
19+
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
20+
) : BaseViewModel<ParamBrowserViewEvent, ParamBrowserViewState, ParamBrowserViewEffect>() {
21+
private var listFoldersFirst = true
22+
private var searchExpression = ""
3623

3724
init {
3825
listFoldersFirst = appPrefs.listFoldersFirst
3926
}
4027

41-
fun setPath(path: String) {
42-
viewModelScope.launch {
43-
loadBrowsableParamFiles(path)
44-
}
45-
}
28+
override fun createInitialState(): ParamBrowserViewState = ParamBrowserViewState()
4629

47-
fun setSearchExpression(expression: String) = updateState {
48-
searchExpression = expression
30+
override fun processEvent(event: ParamBrowserViewEvent) {
31+
when (event) {
32+
ParamBrowserViewEvent.RefreshRequested -> setPath(currentState.currentPath)
33+
is ParamBrowserViewEvent.DirectoryChanged -> onDirectoryChanged(event.dir)
34+
is ParamBrowserViewEvent.SearchExpressionChanged -> searchExpression = event.data
35+
is ParamBrowserViewEvent.ParamClicked -> setEffect {
36+
ParamBrowserViewEffect.NavigateToParamDetails(DomainParamMapper.map(event.param))
37+
}
38+
ParamBrowserViewEvent.DocumentationMenuClicked -> setEffect {
39+
ParamBrowserViewEffect.OpenDocumentationUrl(currentState.docUrl)
40+
}
41+
ParamBrowserViewEvent.FavoritesMenuClicked -> setEffect {
42+
ParamBrowserViewEffect.NavigateToFavorite
43+
}
44+
}
4945
}
5046

51-
fun doWhenParamItemClicked(param: KernelParam, itemLayout: View, activity: Activity) {
52-
val sharedElements = arrayOf<PairUtil<View, String>>(
53-
PairUtil(
54-
itemLayout.findViewById(R.id.name),
55-
EditKernelParamActivity.NAME_TRANSITION_NAME
56-
)
57-
)
58-
val options: ActivityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(
59-
activity,
60-
*sharedElements
61-
)
62-
63-
viewEffect.postValue(ParamBrowserViewEffect.NavigateToParamDetails(param, options))
47+
private fun setPath(path: String) {
48+
viewModelScope.launch {
49+
loadBrowsableParamFiles(path)
50+
}
6451
}
6552

66-
fun doWhenDirectoryChanges(newDir: File) {
53+
private fun onDirectoryChanged(newDir: File) {
6754
val newPath = newDir.absolutePath
6855
if (newPath.isEmpty() || !newPath.startsWith(Consts.PROC_SYS)) {
69-
viewEffect.postValue(ParamBrowserViewEffect.ShowToast(R.string.invalid_path))
56+
setEffect { ParamBrowserViewEffect.ShowToast(R.string.invalid_path) }
7057
return
7158
}
7259

7360
setPath(newPath)
7461

7562
when {
76-
newPath.startsWith("/proc/sys/abi") -> updateState {
77-
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/abi.txt"
78-
showDocumentationMenu = true
63+
newPath.startsWith("/proc/sys/abi") -> setState {
64+
copy(
65+
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/abi.txt",
66+
showDocumentationMenu = true
67+
)
7968
}
8069

81-
newPath.startsWith("/proc/sys/fs") -> updateState {
82-
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/fs.txt"
83-
showDocumentationMenu = true
70+
newPath.startsWith("/proc/sys/fs") -> setState {
71+
copy(
72+
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/fs.txt",
73+
showDocumentationMenu = true
74+
)
8475
}
8576

86-
newPath.startsWith("/proc/sys/kernel") -> updateState {
87-
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/kernel.txt"
88-
showDocumentationMenu = true
77+
newPath.startsWith("/proc/sys/kernel") -> setState {
78+
copy(
79+
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/kernel.txt",
80+
showDocumentationMenu = true
81+
)
8982
}
9083

91-
newPath.startsWith("/proc/sys/net") -> updateState {
92-
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/net.txt"
93-
showDocumentationMenu = true
84+
newPath.startsWith("/proc/sys/net") -> setState {
85+
copy(
86+
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/net.txt",
87+
showDocumentationMenu = true
88+
)
9489
}
9590

96-
newPath.startsWith("/proc/sys/vm") -> updateState {
97-
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/vm.txt"
98-
showDocumentationMenu = true
91+
newPath.startsWith("/proc/sys/vm") -> setState {
92+
copy(
93+
docUrl = "https://www.kernel.org/doc/Documentation/sysctl/vm.txt",
94+
showDocumentationMenu = true
95+
)
9996
}
10097

101-
else -> updateState {
102-
showDocumentationMenu = false
103-
}
98+
else -> setState { copy(showDocumentationMenu = false) }
10499
}
105100
}
106101

107-
fun doWhenDocumentationMenuClicked() {
108-
val url = viewState.value?.docUrl.orEmpty()
109-
viewEffect.postValue(ParamBrowserViewEffect.OpenDocumentationUrl(url))
110-
}
111-
112-
fun doWhenFavoritesMenuClicked() {
113-
viewEffect.postValue(ParamBrowserViewEffect.NavigateToFavorite)
114-
}
115-
116102
private suspend fun getCurrentPathFiles(path: String) = withContext(dispatcher) {
117103
runCatching {
118104
File(path).listFiles()?.toList()
119105
}.getOrDefault(emptyList())
120106
}
121107

122108
private suspend fun loadBrowsableParamFiles(path: String) {
123-
updateState { isLoading = true }
109+
setState { copy(isLoading = true) }
124110
val files = getCurrentPathFiles(path).maybeDirectorySorted().maybeFiltered()
125111
val params = getParamsFromFilesUseCase(files).map {
126112
DomainParamMapper.map(it)
127113
}
128114

129-
updateState {
130-
currentPath = path
131-
isLoading = false
132-
data = params
115+
setState {
116+
copy(currentPath = path, isLoading = false, data = params)
133117
}
134118
}
119+
135120
private suspend fun List<File>?.maybeDirectorySorted() = withContext(dispatcher) {
136121
return@withContext this@maybeDirectorySorted?.run {
137122
if (listFoldersFirst) {
138123
sortedByDescending { it.isDirectory }
139-
} else this
124+
} else {
125+
this
126+
}
140127
}?.toList().orEmpty()
141128
}
142129

143130
private suspend fun List<File>?.maybeFiltered() = withContext(dispatcher) {
144-
val searchExpression = viewState.value?.searchExpression.orEmpty()
145131
return@withContext this@maybeFiltered?.run {
146132
if (searchExpression.isNotEmpty()) {
147133
filter { param ->
148134
param.name.lowercase()
149135
.replace(".", "")
150136
.contains(searchExpression.lowercase())
151137
}
152-
} else this
138+
} else {
139+
this
140+
}
153141
}?.toList().orEmpty()
154142
}
155-
156-
private fun updateState(state: ParamBrowserViewState.() -> Unit) {
157-
_viewState.value = currentViewState.apply(state)
158-
}
159-
160-
private val currentViewState: ParamBrowserViewState
161-
get() = viewState.value ?: ParamBrowserViewState()
162143
}

0 commit comments

Comments
 (0)