@@ -2,13 +2,16 @@ package org.andbootmgr.app.util
22
33import android.util.Log
44import androidx.compose.foundation.horizontalScroll
5+ import androidx.compose.foundation.layout.Column
56import androidx.compose.foundation.layout.fillMaxSize
67import androidx.compose.foundation.layout.padding
78import androidx.compose.foundation.rememberScrollState
89import androidx.compose.foundation.verticalScroll
10+ import androidx.compose.material3.Button
911import androidx.compose.material3.Text
1012import androidx.compose.runtime.Composable
1113import androidx.compose.runtime.LaunchedEffect
14+ import androidx.compose.runtime.MutableState
1215import androidx.compose.runtime.getValue
1316import androidx.compose.runtime.mutableStateOf
1417import androidx.compose.runtime.remember
@@ -18,6 +21,7 @@ import androidx.compose.runtime.setValue
1821import androidx.compose.ui.Modifier
1922import androidx.compose.ui.platform.LocalContext
2023import androidx.compose.ui.platform.LocalLifecycleOwner
24+ import androidx.compose.ui.res.stringResource
2125import androidx.compose.ui.text.font.FontFamily
2226import androidx.compose.ui.unit.dp
2327import kotlinx.coroutines.CoroutineScope
@@ -31,9 +35,12 @@ import java.io.File
3135import java.io.FileOutputStream
3236
3337private class BudgetCallbackList (private val scope : CoroutineScope ,
34- private val log : FileOutputStream ? ) : MutableList<String> {
38+ private val log : FileOutputStream ? )
39+ : MutableList <String >, TerminalList {
40+ override val isCancelled = mutableStateOf<Boolean ?>(null )
41+ override var cancel: (() -> Unit )? = null
3542 val internalList = ArrayList <String >()
36- var cb: ((String ) -> Unit )? = null
43+ var cb: (() -> Unit )? = null
3744 override val size: Int
3845 get() = internalList.size
3946
@@ -118,7 +125,9 @@ private class BudgetCallbackList(private val scope: CoroutineScope,
118125 }
119126
120127 override fun set (index : Int , element : String ): String {
121- return internalList.set(index, element)
128+ return internalList.set(index, element).also {
129+ cb?.invoke()
130+ }
122131 }
123132
124133 override fun subList (fromIndex : Int , toIndex : Int ): MutableList <String > {
@@ -129,18 +138,26 @@ private class BudgetCallbackList(private val scope: CoroutineScope,
129138 scope.launch {
130139 log?.write((element + " \n " ).encodeToByteArray())
131140 }
132- cb?.invoke(element )
141+ cb?.invoke()
133142 }
134143}
135144
145+ interface TerminalList : MutableList <String > {
146+ val isCancelled: MutableState <Boolean ?>
147+ var cancel: (() -> Unit )?
148+ }
149+ class TerminalCancelException : RuntimeException ()
150+
136151/* Monospace auto-scrolling text view, fed using MutableList<String>, catching exceptions and running logic on a different thread */
137152@OptIn(ExperimentalCoroutinesApi ::class )
138153@Composable
139154fun Terminal (logFile : String? = null, doWhenDone : (() -> Unit )? = null,
140- action : (suspend (MutableList < String > ) -> Unit )? ) {
155+ action : (suspend (TerminalList ) -> Unit )? ) {
141156 val scrollH = rememberScrollState()
142157 val scrollV = rememberScrollState()
143- val scope = rememberCoroutineScope()
158+ val scope = rememberCoroutineScope { Dispatchers .Main }
159+ var isCancelledState by remember { mutableStateOf(mutableStateOf<Boolean ?>(null )) }
160+ var doCancelState by remember { mutableStateOf< (() -> Unit )? > (null ) }
144161 var didConnectAndFinish by rememberSaveable { mutableStateOf(false ) }
145162 var text by rememberSaveable { mutableStateOf(" " ) }
146163 val ctx = LocalContext .current.applicationContext
@@ -161,9 +178,12 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null,
161178 val logDispatcher = Dispatchers .IO .limitedParallelism(1 )
162179 val log = logFile?.let { FileOutputStream (File (ctx.externalCacheDir, it)) }
163180 val s = BudgetCallbackList (CoroutineScope (logDispatcher), log)
164- s.cb = { element ->
181+ isCancelledState = s.isCancelled
182+ doCancelState = { s.cancel!! () }
183+ s.cb = {
184+ val l = s.toList()
165185 scope.launch {
166- text + = element + " \n "
186+ text = l.joinToString( " \n " ). let { if (s.isNotEmpty()) it + " \n " else it }
167187 delay(200 ) // Give it time to re-measure
168188 scrollV.animateScrollTo(scrollV.maxValue)
169189 scrollH.animateScrollTo(0 )
@@ -173,6 +193,8 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null,
173193 withContext(Dispatchers .Default ) {
174194 try {
175195 action(s)
196+ } catch (e: TerminalCancelException ) {
197+ s.add(ctx.getString(R .string.install_canceled))
176198 } catch (e: Throwable ) {
177199 s.add(ctx.getString(R .string.term_failure))
178200 s.add(ctx.getString(R .string.dev_details))
@@ -185,22 +207,37 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null,
185207 }, s)
186208 } else {
187209 val s = service.workExtra as BudgetCallbackList
210+ isCancelledState = s.isCancelled
211+ doCancelState = { s.cancel!! () }
188212 text = s.joinToString(" \n " ).let { if (s.isNotEmpty()) it + " \n " else it }
189- s.cb = { element ->
213+ s.cb = {
214+ val l = s.toList()
190215 scope.launch {
191- text + = element + " \n "
216+ text = l.joinToString( " \n " ). let { if (s.isNotEmpty()) it + " \n " else it }
192217 delay(200 ) // Give it time to re-measure
193218 scrollV.animateScrollTo(scrollV.maxValue)
194219 scrollH.animateScrollTo(0 )
195220 }
196221 }
222+
197223 }
198224 }
199225 }
200226 }
201- Text (text, modifier = Modifier
202- .fillMaxSize()
203- .horizontalScroll(scrollH)
204- .verticalScroll(scrollV)
205- .padding(10 .dp), fontFamily = FontFamily .Monospace )
227+ Column (modifier = Modifier .fillMaxSize()) {
228+ Text (text, modifier = Modifier
229+ .fillMaxSize()
230+ .weight(1f )
231+ .horizontalScroll(scrollH)
232+ .verticalScroll(scrollV)
233+ .padding(10 .dp), fontFamily = FontFamily .Monospace
234+ )
235+ if (isCancelledState.value == false ) {
236+ Button ({
237+ doCancelState?.invoke()
238+ }) {
239+ Text (stringResource(R .string.cancel))
240+ }
241+ }
242+ }
206243}
0 commit comments