Added network syncing

master
Wynd 2026-02-22 00:47:44 +02:00
parent 6b8ac9270f
commit a6105c9ced
6 changed files with 156 additions and 39 deletions

View File

@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@ -11,14 +11,14 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.room.Room
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.data.AppDatabase
import xyz.pixelatedw.recipe.data.RecipesView
import xyz.pixelatedw.recipe.ui.components.MainScreen
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.utils.parseRecipes
class MainActivity : ComponentActivity() {
private val recipeView: RecipesView by viewModels()
val recipeView: RecipesView by viewModels()
lateinit var db: AppDatabase
override fun onCreate(savedInstanceState: Bundle?) {
@ -49,7 +49,7 @@ class MainActivity : ComponentActivity() {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
parseRecipes(this, db, uri)
parseRecipes(this, uri)
val recipes = db.recipeWithTagsDao().getAll()
recipeView.setRecipes(recipes)

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity

View File

@ -1,6 +1,7 @@
package xyz.pixelatedw.recipe.ui.components
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -25,7 +26,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
@ -35,6 +35,7 @@ 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.sync
import java.io.File
@Composable
@ -131,7 +132,8 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
} else {
IconButton(
modifier = Modifier.weight(0.15f),
onClick = { ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) },
onClick = { sync(ctx) },
// onClick = { ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) },
) {
Icon(
imageVector = Icons.Default.Add,

View File

@ -5,7 +5,7 @@ import android.net.Uri
import androidx.core.text.trimmedLength
import androidx.documentfile.provider.DocumentFile
import io.github.wasabithumb.jtoml.JToml
import xyz.pixelatedw.recipe.data.AppDatabase
import xyz.pixelatedw.recipe.MainActivity
import xyz.pixelatedw.recipe.data.Recipe
import xyz.pixelatedw.recipe.data.RecipeTag
import xyz.pixelatedw.recipe.data.Tag
@ -16,7 +16,7 @@ import java.io.InputStreamReader
private val recipeFiles = mutableListOf<DocumentFile>()
fun parseRecipes(ctx: Context, db: AppDatabase, uri: Uri?) {
fun parseRecipes(ctx: MainActivity, uri: Uri?) {
if (uri == null) {
return
}
@ -26,10 +26,21 @@ fun parseRecipes(ctx: Context, db: AppDatabase, uri: Uri?) {
val dir = DocumentFile.fromTreeUri(ctx, uri)
if (dir != null) {
parseDir(ctx, dir, path)
for (file in recipeFiles) {
parseRecipe(ctx, db, file)
parseRecipeFiles(ctx)
}
}
fun parseRecipeFiles(ctx: MainActivity) {
for (file in recipeFiles) {
val lastModified = file.lastModified()
val inputStream = ctx.contentResolver.openInputStream(file.uri)
val reader = BufferedReader(InputStreamReader(inputStream))
val text = reader.readText()
parseRecipe(ctx, lastModified, text)
reader.close()
}
}
@ -41,11 +52,26 @@ fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
continue
}
if (file.isFile && file.name?.endsWith(".jpg") == true) {
handleFile(ctx, file, path)
}
}
fun handleFile(ctx: Context, file: DocumentFile, path: String) {
if (!file.isFile) {
return
}
val fileName = file.name ?: return
// println("file: ${file.name} | ${file.uri.path} | ${file.length()}")
if (fileName.endsWith(".jpg")) {
val picsDir = File(ctx.filesDir, path)
picsDir.mkdirs()
val newFile = File(picsDir, file.name!!)
val newFile = File(picsDir, fileName)
if (newFile.exists()) {
return
}
ctx.contentResolver.openInputStream(file.uri).use { inStream ->
FileOutputStream(newFile, false).use { outStream ->
@ -53,21 +79,14 @@ fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
}
}
}
if (file.isFile && file.name?.endsWith(".md") == true) {
else if (fileName.endsWith(".md")) {
recipeFiles.add(file)
}
}
}
private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
val lastModified = file.lastModified()
val inputStream = ctx.contentResolver.openInputStream(file.uri)
val reader = BufferedReader(InputStreamReader(inputStream))
val text = reader.readText()
fun parseRecipe(ctx: MainActivity, lastModified: Long, text: String) {
val hash = text.hashCode() // Probably not the best way but its stable and works
if (db.recipeDao().hasHash(hash)) {
if (ctx.db.recipeDao().hasHash(hash)) {
return
}
@ -92,7 +111,6 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
}
if (!hasToml) {
reader.close()
return
}
@ -100,7 +118,6 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
val doc = toml.readFromString(sb.toString())
if (!doc.contains("title")) {
reader.close()
return
}
@ -116,15 +133,15 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
val tags = arrayListOf<Tag>()
if (doc.contains("tags")) {
// Delete already existing tags for this recipe
db.recipeWithTagsDao().delete(recipeTitle)
ctx.db.recipeWithTagsDao().delete(recipeTitle)
for (tomlElem in doc["tags"]!!.asArray()) {
val tag = Tag(tomlElem!!.asPrimitive().asString())
tags.add(tag)
val recipeWithTags = RecipeTag(recipeTitle, tag.name)
db.tagDao().insert(tag)
db.recipeWithTagsDao().insert(recipeWithTags)
ctx.db.tagDao().insert(tag)
ctx.db.recipeWithTagsDao().insert(recipeWithTags)
}
}
@ -138,7 +155,5 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
hash = hash
)
db.recipeDao().insert(recipe)
reader.close()
ctx.db.recipeDao().insert(recipe)
}

View File

@ -0,0 +1,96 @@
package xyz.pixelatedw.recipe.utils
import android.os.Build
import android.os.FileUtils
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.documentfile.provider.DocumentFile
import xyz.pixelatedw.recipe.MainActivity
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.File
import java.io.FileOutputStream
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors
fun sync(ctx: MainActivity) {
val executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper())
executor.execute(Runnable() {
try {
val conn = Socket("192.168.0.100", 9696)
val stream = conn.getInputStream()
val inputStream = DataInputStream(stream)
var buffer = ByteArray(8)
inputStream.read(buffer)
val filesSent = ByteBuffer.wrap(buffer).getLong().toInt()
// println("files sent: $filesSent")
for(f in 0..<filesSent) {
buffer = ByteArray(8)
inputStream.read(buffer)
val nameBufLen = ByteBuffer.wrap(buffer).getLong().toInt()
buffer = ByteArray(nameBufLen)
inputStream.read(buffer)
val fileFullPath = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(buffer)).toString();
val fileName = fileFullPath.split("/").last()
val filePath = fileFullPath.replace(fileName, "")
buffer = ByteArray(8)
inputStream.read(buffer)
val contentBufLen = ByteBuffer.wrap(buffer).getLong().toInt()
var parentDir = ctx.filesDir
if (filePath.isNotEmpty()) {
parentDir = File(ctx.filesDir, filePath)
parentDir.mkdirs()
}
val newFile = File(parentDir, fileName)
val fos = FileOutputStream(newFile, false)
var usedBytes = 0
var blockSize = 1024
while (usedBytes != contentBufLen) {
if (usedBytes + blockSize > contentBufLen) {
blockSize = contentBufLen - usedBytes
}
else if (blockSize > contentBufLen) {
blockSize = contentBufLen
}
val contentBuffer = ByteArray(blockSize)
val readBytes = inputStream.read(contentBuffer)
fos.write(contentBuffer, 0, readBytes)
fos.flush()
usedBytes += readBytes
}
fos.close()
// println("new file: ${newFile.absolutePath} | $contentBufLen | ${newFile.length()}")
}
val docDir = DocumentFile.fromFile(ctx.filesDir)
parseDir(ctx, docDir, "")
parseRecipeFiles(ctx)
} catch (e: Exception) {
e.printStackTrace()
}
handler.post(Runnable {
val recipes = ctx.db.recipeWithTagsDao().getAll()
ctx.recipeView.setRecipes(recipes)
println("syncing complete")
})
})
}