Added network syncing
parent
6b8ac9270f
commit
a6105c9ced
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,11 +26,22 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue