Added network syncing
parent
6b8ac9270f
commit
a6105c9ced
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,10 +26,21 @@ 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) {
|
fun parseRecipeFiles(ctx: MainActivity) {
|
||||||
parseRecipe(ctx, db, file)
|
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,33 +52,41 @@ fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.isFile && file.name?.endsWith(".jpg") == true) {
|
handleFile(ctx, file, path)
|
||||||
val picsDir = File(ctx.filesDir, path)
|
|
||||||
picsDir.mkdirs()
|
|
||||||
|
|
||||||
val newFile = File(picsDir, file.name!!)
|
|
||||||
|
|
||||||
ctx.contentResolver.openInputStream(file.uri).use { inStream ->
|
|
||||||
FileOutputStream(newFile, false).use { outStream ->
|
|
||||||
inStream?.copyTo(outStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (file.isFile && file.name?.endsWith(".md") == true) {
|
|
||||||
recipeFiles.add(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) {
|
fun handleFile(ctx: Context, file: DocumentFile, path: String) {
|
||||||
val lastModified = file.lastModified()
|
if (!file.isFile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val inputStream = ctx.contentResolver.openInputStream(file.uri)
|
val fileName = file.name ?: return
|
||||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
// println("file: ${file.name} | ${file.uri.path} | ${file.length()}")
|
||||||
val text = reader.readText()
|
|
||||||
|
|
||||||
|
if (fileName.endsWith(".jpg")) {
|
||||||
|
val picsDir = File(ctx.filesDir, path)
|
||||||
|
picsDir.mkdirs()
|
||||||
|
|
||||||
|
val newFile = File(picsDir, fileName)
|
||||||
|
if (newFile.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contentResolver.openInputStream(file.uri).use { inStream ->
|
||||||
|
FileOutputStream(newFile, false).use { outStream ->
|
||||||
|
inStream?.copyTo(outStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fileName.endsWith(".md")) {
|
||||||
|
recipeFiles.add(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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