diff --git a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt index bc81409..6e9ce6b 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt @@ -21,6 +21,9 @@ class MainActivity : ComponentActivity() { val recipeView: RecipesView by viewModels() lateinit var db: AppDatabase + var importFinished: Boolean = false + var importError: String = "" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,10 +53,22 @@ class MainActivity : ComponentActivity() { if (result.resultCode == RESULT_OK) { result.data?.data?.let { uri -> parseRecipes(this, uri) - - val recipes = db.recipeWithTagsDao().getAll() - recipeView.setRecipes(recipes) + importError = "" } } + else { + importError = "Failed to import recipes (${result.resultCode})" + } + + importFinished = true } + + fun getAppVersion(): String { + val info = this.packageManager.getPackageInfo(packageName, 0) + return if (info.versionName == null) { + "Unknown" + } else { + "v${info.versionName}" + } + } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/ImportDropdownMenu.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/ImportDropdownMenu.kt deleted file mode 100644 index 80b25a5..0000000 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/ImportDropdownMenu.kt +++ /dev/null @@ -1,55 +0,0 @@ -package xyz.pixelatedw.recipe.ui.components - -import android.content.Intent -import androidx.compose.foundation.layout.Box -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import xyz.pixelatedw.recipe.MainActivity -import xyz.pixelatedw.recipe.utils.sync - -@Composable -fun ImportDropdownMenu(ctx: MainActivity, modifier: Modifier) { - var expanded by remember { mutableStateOf(false) } - Box( - modifier = modifier, - ) { - IconButton( - onClick = { expanded = !expanded } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Import recipes" - ) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { Text("Import") }, - onClick = { - ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) - expanded = false - } - ) - DropdownMenuItem( - text = { Text("Sync") }, - onClick = { - sync(ctx) - expanded = false - } - ) - } - } -} diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt index e0cc427..2aee0e4 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt @@ -1,6 +1,5 @@ package xyz.pixelatedw.recipe.ui.components -import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -9,19 +8,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -30,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -39,8 +33,6 @@ import xyz.pixelatedw.recipe.MainActivity import xyz.pixelatedw.recipe.R import xyz.pixelatedw.recipe.data.RecipeWithTags import xyz.pixelatedw.recipe.data.RecipesView -import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_IP -import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_PORT import java.io.File @@ -51,11 +43,6 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { val search = view.search.collectAsState() val filters = view.tagFilters.collectAsState() - val prefs = ctx.getPreferences(Context.MODE_PRIVATE) - - var syncIp by remember { mutableStateOf(prefs.getString("syncServerIp", DEFAULT_SYNC_SERVER_IP)!!) } - var syncPort by remember { mutableIntStateOf(prefs.getInt("syncServerPort", DEFAULT_SYNC_SERVER_PORT)) } - val navController = rememberNavController() var openDeletionDialog by remember { mutableStateOf(false) } @@ -106,29 +93,19 @@ 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) }, ) - // Tags - IconButton( - modifier = Modifier.weight(0.1f), - onClick = { openTagFilterDialog = true }, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.filter_24), - contentDescription = "Toggle tags filtering menu" - ) - } - // Import / Delete + // Tags / Delete if (selectedEntries.isNotEmpty()) { IconButton( modifier = Modifier.weight(0.1f), @@ -143,7 +120,15 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { ) } } else { - ImportDropdownMenu(ctx, Modifier.weight(0.1f)) + IconButton( + modifier = Modifier.weight(0.1f), + onClick = { openTagFilterDialog = true }, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.filter_24), + contentDescription = "Toggle tags filtering menu" + ) + } } // Options IconButton( @@ -217,42 +202,7 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { RecipeInfo(ctx, view, navController, padding, active.value!!) } composable("settings") { - Column(modifier = Modifier.padding(start = 12.dp, top = 48.dp)) { - OutlinedTextField( - label = { Text("Sync Server IP") }, - singleLine = true, - value = syncIp, - onValueChange = { syncIp = it }, - ) - - OutlinedTextField( - modifier = Modifier.padding(top = 8.dp), - label = { Text("Sync Server IP") }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - value = syncPort.toString(), - onValueChange = { - syncPort = when (it.toIntOrNull()) { - null -> syncPort - else -> it.toInt() - } - }, - ) - - Button( - modifier = Modifier.padding(top = 8.dp), - onClick = { - with (prefs.edit()) { - putString("syncServerIp", syncIp) - putInt("syncServerPort", syncPort) - apply() - } - navController.navigate("list") - }, - ) { - Text("Save") - } - } + SettingsScreen(ctx, navController) } } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/SettingsScreen.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/SettingsScreen.kt new file mode 100644 index 0000000..a7dc46c --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/SettingsScreen.kt @@ -0,0 +1,113 @@ +package xyz.pixelatedw.recipe.ui.components + +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import xyz.pixelatedw.recipe.MainActivity +import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_IP +import xyz.pixelatedw.recipe.utils.DEFAULT_SYNC_SERVER_PORT +import xyz.pixelatedw.recipe.utils.import +import xyz.pixelatedw.recipe.utils.sync + +@Composable +fun SettingsScreen(ctx: MainActivity, nav: NavHostController) { + val prefs = ctx.getPreferences(Context.MODE_PRIVATE) + + var syncIp by remember { + mutableStateOf( + prefs.getString( + "syncServerIp", + DEFAULT_SYNC_SERVER_IP + )!! + ) + } + var syncPort by remember { + mutableIntStateOf( + prefs.getInt( + "syncServerPort", + DEFAULT_SYNC_SERVER_PORT + ) + ) + } + + Column( + modifier = Modifier.padding(start = 12.dp, top = 48.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedTextField( + label = { Text("Sync Server IP") }, + singleLine = true, + value = syncIp, + onValueChange = { syncIp = it }, + ) + + OutlinedTextField( + label = { Text("Sync Server Port") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + value = syncPort.toString(), + onValueChange = { + syncPort = when (it.toIntOrNull()) { + null -> syncPort + else -> it.toInt() + } + }, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button(onClick = { + import(ctx, nav) +// ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) + }) { + Text("Import") + } + + Button(onClick = { sync(ctx, nav) }) { + Text("Sync") + } + } + + Button( + onClick = { + with(prefs.edit()) { + putString("syncServerIp", syncIp) + putInt("syncServerPort", syncPort) + apply() + } + nav.navigate("list") + }, + ) { + Text("Save") + } + + Row( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 64.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text(ctx.getAppVersion()) + } + } +} diff --git a/app/src/main/java/xyz/pixelatedw/recipe/utils/ImportRecipes.kt b/app/src/main/java/xyz/pixelatedw/recipe/utils/ImportRecipes.kt new file mode 100644 index 0000000..57a5987 --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/utils/ImportRecipes.kt @@ -0,0 +1,38 @@ +package xyz.pixelatedw.recipe.utils + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.navigation.NavHostController +import xyz.pixelatedw.recipe.MainActivity +import java.util.concurrent.Executors + +fun import(ctx: MainActivity, nav: NavHostController) { + val executor = Executors.newSingleThreadExecutor() + val handler = Handler(Looper.getMainLooper()) + + executor.execute { + ctx.importFinished = false + + try { + ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) + // Keep the thread busy until the import is done + // 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) { + e.printStackTrace() + } + + handler.post { + if (ctx.importError.isEmpty()) { + val recipes = ctx.db.recipeWithTagsDao().getAll() + ctx.recipeView.setRecipes(recipes) + nav.navigate("list") +// println("importing complete") + } + else { + println(ctx.importError) + } + } + } +} diff --git a/app/src/main/java/xyz/pixelatedw/recipe/utils/SyncRecipes.kt b/app/src/main/java/xyz/pixelatedw/recipe/utils/SyncRecipes.kt index 8cd1c51..ad323a6 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/utils/SyncRecipes.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/utils/SyncRecipes.kt @@ -4,19 +4,23 @@ import android.content.Context import android.os.Handler import android.os.Looper import androidx.documentfile.provider.DocumentFile +import androidx.navigation.NavHostController import xyz.pixelatedw.recipe.MainActivity import java.io.DataInputStream import java.io.File import java.io.FileOutputStream +import java.net.InetSocketAddress import java.net.Socket +import java.net.SocketTimeoutException import java.nio.ByteBuffer import java.nio.charset.StandardCharsets import java.util.concurrent.Executors 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) { +fun sync(ctx: MainActivity, nav: NavHostController) { val executor = Executors.newSingleThreadExecutor() val handler = Handler(Looper.getMainLooper()) @@ -26,7 +30,8 @@ fun sync(ctx: MainActivity) { val syncServerIp = prefs.getString("syncServerIp", DEFAULT_SYNC_SERVER_IP) val syncServerPort = prefs.getInt("syncServerPort", DEFAULT_SYNC_SERVER_PORT) - val conn = Socket(syncServerIp, syncServerPort) + val conn = Socket() + conn.connect(InetSocketAddress(syncServerIp, syncServerPort), CONNECTION_TIMEOUT) val stream = conn.getInputStream() val inputStream = DataInputStream(stream) @@ -86,13 +91,23 @@ fun sync(ctx: MainActivity) { parseRecipeFiles(ctx) } catch (e: Exception) { + ctx.importError = when (e) { + is SocketTimeoutException -> "Connection timed out" + else -> "Error occurred: ${e.javaClass.canonicalName}" + } e.printStackTrace() } handler.post { - val recipes = ctx.db.recipeWithTagsDao().getAll() - ctx.recipeView.setRecipes(recipes) -// println("syncing complete") + if (ctx.importError.isEmpty()) { + val recipes = ctx.db.recipeWithTagsDao().getAll() + ctx.recipeView.setRecipes(recipes) + nav.navigate("list") +// println("syncing complete") + } + else { + println(ctx.importError) + } } } }