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) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
parseRecipes(this, uri) parseRecipes(this, uri)
importError = ""
} }
} }
else { else {
importError = "Failed to import recipes (${result.resultCode})" importError = "Import cancelled"
} }
importFinished = true 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

@ -133,7 +133,10 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
// Options // Options
IconButton( IconButton(
modifier = Modifier.weight(0.1f), modifier = Modifier.weight(0.1f),
onClick = { navController.navigate("settings") }, onClick = {
ctx.importError = ""
navController.navigate("settings")
},
) { ) {
Icon( Icon(
imageVector = Icons.Default.Settings, imageVector = Icons.Default.Settings,

View File

@ -18,8 +18,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType 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.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import xyz.pixelatedw.recipe.MainActivity import xyz.pixelatedw.recipe.MainActivity
import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_IP 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( Column(
modifier = Modifier.padding(start = 12.dp, top = 48.dp), modifier = Modifier.padding(start = 12.dp, top = 48.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
@ -76,13 +101,14 @@ fun SettingsScreen(ctx: MainActivity, nav: NavHostController) {
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Button(onClick = { Button(onClick = {
import(ctx, nav) import(ctx, nav, setLoading)
// ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
}) { }) {
Text("Import") Text("Import")
} }
Button(onClick = { sync(ctx, nav) }) { Button(onClick = {
sync(ctx, nav, setLoading)
}) {
Text("Sync") Text("Sync")
} }
} }

View File

@ -7,11 +7,13 @@ import androidx.navigation.NavHostController
import xyz.pixelatedw.recipe.MainActivity import xyz.pixelatedw.recipe.MainActivity
import java.util.concurrent.Executors 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 executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
executor.execute { executor.execute {
setLoading(true)
ctx.importError = ""
ctx.importFinished = false ctx.importFinished = false
try { 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 // TODO This feels dirty but it doesn't block the main thread and I can't figure out a better way
while(!ctx.importFinished) {} while(!ctx.importFinished) {}
} catch (e: Exception) { } catch (e: Exception) {
ctx.importError = when (e) {
else -> "Exception occurred: ${e.javaClass.canonicalName}"
}
e.printStackTrace() e.printStackTrace()
} }
@ -28,11 +33,9 @@ fun import(ctx: MainActivity, nav: NavHostController) {
val recipes = ctx.db.recipeWithTagsDao().getAll() val recipes = ctx.db.recipeWithTagsDao().getAll()
ctx.recipeView.setRecipes(recipes) ctx.recipeView.setRecipes(recipes)
nav.navigate("list") 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 DEFAULT_SYNC_SERVER_PORT = 9696
const val CONNECTION_TIMEOUT = 1_000 // 1 seconds 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 executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
executor.execute { executor.execute {
setLoading(true)
ctx.importError = ""
try { try {
val prefs = ctx.getPreferences(Context.MODE_PRIVATE) val prefs = ctx.getPreferences(Context.MODE_PRIVATE)
val syncServerIp = prefs.getString("syncServerIp", DEFAULT_SYNC_SERVER_IP) 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()}") // println("new file: ${newFile.absolutePath} | $contentBufLen | ${newFile.length()}")
} }
inputStream.close()
val docDir = DocumentFile.fromFile(ctx.filesDir) val docDir = DocumentFile.fromFile(ctx.filesDir)
parseDir(ctx, docDir, "") parseDir(ctx, docDir, "")
@ -93,7 +97,7 @@ fun sync(ctx: MainActivity, nav: NavHostController) {
} catch (e: Exception) { } catch (e: Exception) {
ctx.importError = when (e) { ctx.importError = when (e) {
is SocketTimeoutException -> "Connection timed out" is SocketTimeoutException -> "Connection timed out"
else -> "Error occurred: ${e.javaClass.canonicalName}" else -> "Exception occurred: ${e.javaClass.canonicalName}"
} }
e.printStackTrace() e.printStackTrace()
} }
@ -103,11 +107,9 @@ fun sync(ctx: MainActivity, nav: NavHostController) {
val recipes = ctx.db.recipeWithTagsDao().getAll() val recipes = ctx.db.recipeWithTagsDao().getAll()
ctx.recipeView.setRecipes(recipes) ctx.recipeView.setRecipes(recipes)
nav.navigate("list") nav.navigate("list")
// println("syncing complete")
}
else {
println(ctx.importError)
} }
setLoading(false)
} }
} }
} }