Added a loading indicator and error message for failed imports/syncs

master
Wynd 2026-02-28 15:05:02 +02:00
parent fcb18018d4
commit 9f740e5bcc
6 changed files with 80 additions and 21 deletions

View File

@ -53,11 +53,10 @@ class MainActivity : ComponentActivity() {
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
parseRecipes(this, uri)
importError = ""
}
}
else {
importError = "Failed to import recipes (${result.resultCode})"
importError = "Import cancelled"
}
importFinished = true

View File

@ -0,0 +1,26 @@
package xyz.pixelatedw.recipe.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingIndicator(isLoading: Boolean) {
if (!isLoading) return
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.width(128.dp),
)
}
}

View File

@ -93,15 +93,15 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
Column(modifier = Modifier.padding(padding)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
OutlinedTextField(
modifier = Modifier
.weight(0.7f)
.padding(end = 4.dp),
.weight(0.7f)
.padding(end = 4.dp),
value = search.value.orEmpty(),
onValueChange = { search -> view.setSearch(search) },
)
@ -133,7 +133,10 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
// Options
IconButton(
modifier = Modifier.weight(0.1f),
onClick = { navController.navigate("settings") },
onClick = {
ctx.importError = ""
navController.navigate("settings")
},
) {
Icon(
imageVector = Icons.Default.Settings,

View File

@ -18,8 +18,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import xyz.pixelatedw.recipe.MainActivity
import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_IP
@ -48,6 +51,28 @@ fun SettingsScreen(ctx: MainActivity, nav: NavHostController) {
)
}
val (isLoading, setLoading) = remember { mutableStateOf(false) }
LoadingIndicator(isLoading)
// Error text field
Row(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 128.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Bottom
) {
Text(
ctx.importError,
color = Color.Red,
fontSize = 32.sp,
softWrap = true,
textAlign = TextAlign.Center,
lineHeight = 32.sp
)
}
Column(
modifier = Modifier.padding(start = 12.dp, top = 48.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
@ -76,13 +101,14 @@ fun SettingsScreen(ctx: MainActivity, nav: NavHostController) {
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = {
import(ctx, nav)
// ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
import(ctx, nav, setLoading)
}) {
Text("Import")
}
Button(onClick = { sync(ctx, nav) }) {
Button(onClick = {
sync(ctx, nav, setLoading)
}) {
Text("Sync")
}
}

View File

@ -7,11 +7,13 @@ import androidx.navigation.NavHostController
import xyz.pixelatedw.recipe.MainActivity
import java.util.concurrent.Executors
fun import(ctx: MainActivity, nav: NavHostController) {
fun import(ctx: MainActivity, nav: NavHostController, setLoading: (Boolean) -> Unit) {
val executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper())
executor.execute {
setLoading(true)
ctx.importError = ""
ctx.importFinished = false
try {
@ -20,6 +22,9 @@ fun import(ctx: MainActivity, nav: NavHostController) {
// TODO This feels dirty but it doesn't block the main thread and I can't figure out a better way
while(!ctx.importFinished) {}
} catch (e: Exception) {
ctx.importError = when (e) {
else -> "Exception occurred: ${e.javaClass.canonicalName}"
}
e.printStackTrace()
}
@ -28,11 +33,9 @@ fun import(ctx: MainActivity, nav: NavHostController) {
val recipes = ctx.db.recipeWithTagsDao().getAll()
ctx.recipeView.setRecipes(recipes)
nav.navigate("list")
// println("importing complete")
}
else {
println(ctx.importError)
}
setLoading(false)
}
}
}

View File

@ -20,11 +20,14 @@ const val DEFAULT_SYNC_SERVER_IP = "192.168.0.100"
const val DEFAULT_SYNC_SERVER_PORT = 9696
const val CONNECTION_TIMEOUT = 1_000 // 1 seconds
fun sync(ctx: MainActivity, nav: NavHostController) {
fun sync(ctx: MainActivity, nav: NavHostController, setLoading: (Boolean) -> Unit) {
val executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper())
executor.execute {
setLoading(true)
ctx.importError = ""
try {
val prefs = ctx.getPreferences(Context.MODE_PRIVATE)
val syncServerIp = prefs.getString("syncServerIp", DEFAULT_SYNC_SERVER_IP)
@ -85,6 +88,7 @@ fun sync(ctx: MainActivity, nav: NavHostController) {
// println("new file: ${newFile.absolutePath} | $contentBufLen | ${newFile.length()}")
}
inputStream.close()
val docDir = DocumentFile.fromFile(ctx.filesDir)
parseDir(ctx, docDir, "")
@ -93,7 +97,7 @@ fun sync(ctx: MainActivity, nav: NavHostController) {
} catch (e: Exception) {
ctx.importError = when (e) {
is SocketTimeoutException -> "Connection timed out"
else -> "Error occurred: ${e.javaClass.canonicalName}"
else -> "Exception occurred: ${e.javaClass.canonicalName}"
}
e.printStackTrace()
}
@ -103,11 +107,9 @@ fun sync(ctx: MainActivity, nav: NavHostController) {
val recipes = ctx.db.recipeWithTagsDao().getAll()
ctx.recipeView.setRecipes(recipes)
nav.navigate("list")
// println("syncing complete")
}
else {
println(ctx.importError)
}
setLoading(false)
}
}
}