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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" 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.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.room.Room import androidx.room.Room
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.data.AppDatabase import xyz.pixelatedw.recipe.data.AppDatabase
import xyz.pixelatedw.recipe.data.RecipesView import xyz.pixelatedw.recipe.data.RecipesView
import xyz.pixelatedw.recipe.ui.components.MainScreen import xyz.pixelatedw.recipe.ui.components.MainScreen
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.utils.parseRecipes import xyz.pixelatedw.recipe.utils.parseRecipes
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val recipeView: RecipesView by viewModels() val recipeView: RecipesView by viewModels()
lateinit var db: AppDatabase lateinit var db: AppDatabase
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -49,7 +49,7 @@ class MainActivity : ComponentActivity() {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
parseRecipes(this, db, uri) parseRecipes(this, uri)
val recipes = db.recipeWithTagsDao().getAll() val recipes = db.recipeWithTagsDao().getAll()
recipeView.setRecipes(recipes) recipeView.setRecipes(recipes)

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import android.net.Uri
import androidx.core.text.trimmedLength import androidx.core.text.trimmedLength
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.github.wasabithumb.jtoml.JToml 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.Recipe
import xyz.pixelatedw.recipe.data.RecipeTag import xyz.pixelatedw.recipe.data.RecipeTag
import xyz.pixelatedw.recipe.data.Tag import xyz.pixelatedw.recipe.data.Tag
@ -16,7 +16,7 @@ import java.io.InputStreamReader
private val recipeFiles = mutableListOf<DocumentFile>() private val recipeFiles = mutableListOf<DocumentFile>()
fun parseRecipes(ctx: Context, db: AppDatabase, uri: Uri?) { fun parseRecipes(ctx: MainActivity, uri: Uri?) {
if (uri == null) { if (uri == null) {
return return
} }
@ -26,11 +26,22 @@ fun parseRecipes(ctx: Context, db: AppDatabase, uri: Uri?) {
val dir = DocumentFile.fromTreeUri(ctx, uri) val dir = DocumentFile.fromTreeUri(ctx, uri)
if (dir != null) { if (dir != null) {
parseDir(ctx, dir, path) parseDir(ctx, dir, path)
parseRecipeFiles(ctx)
for (file in recipeFiles) {
parseRecipe(ctx, db, file)
} }
} }
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()
}
} }
fun parseDir(ctx: Context, dir: DocumentFile, path: String) { fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
@ -41,11 +52,26 @@ fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
continue 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) val picsDir = File(ctx.filesDir, path)
picsDir.mkdirs() picsDir.mkdirs()
val newFile = File(picsDir, file.name!!) val newFile = File(picsDir, fileName)
if (newFile.exists()) {
return
}
ctx.contentResolver.openInputStream(file.uri).use { inStream -> ctx.contentResolver.openInputStream(file.uri).use { inStream ->
FileOutputStream(newFile, false).use { outStream -> 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) 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 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 return
} }
@ -92,7 +111,6 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
} }
if (!hasToml) { if (!hasToml) {
reader.close()
return return
} }
@ -100,7 +118,6 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
val doc = toml.readFromString(sb.toString()) val doc = toml.readFromString(sb.toString())
if (!doc.contains("title")) { if (!doc.contains("title")) {
reader.close()
return return
} }
@ -116,15 +133,15 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
val tags = arrayListOf<Tag>() val tags = arrayListOf<Tag>()
if (doc.contains("tags")) { if (doc.contains("tags")) {
// Delete already existing tags for this recipe // Delete already existing tags for this recipe
db.recipeWithTagsDao().delete(recipeTitle) ctx.db.recipeWithTagsDao().delete(recipeTitle)
for (tomlElem in doc["tags"]!!.asArray()) { for (tomlElem in doc["tags"]!!.asArray()) {
val tag = Tag(tomlElem!!.asPrimitive().asString()) val tag = Tag(tomlElem!!.asPrimitive().asString())
tags.add(tag) tags.add(tag)
val recipeWithTags = RecipeTag(recipeTitle, tag.name) val recipeWithTags = RecipeTag(recipeTitle, tag.name)
db.tagDao().insert(tag) ctx.db.tagDao().insert(tag)
db.recipeWithTagsDao().insert(recipeWithTags) ctx.db.recipeWithTagsDao().insert(recipeWithTags)
} }
} }
@ -138,7 +155,5 @@ private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
hash = hash hash = hash
) )
db.recipeDao().insert(recipe) ctx.db.recipeDao().insert(recipe)
reader.close()
} }

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")
})
})
}