diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3267bd3..3d08cfb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,10 @@ android { } buildTypes { + debug { + applicationIdSuffix = ".theo" + resValue("string", "app_name", "Theotracker") + } release { isMinifyEnabled = false proguardFiles( @@ -52,6 +56,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.recyclerview) implementation("com.github.thegrizzlylabs:sardine-android:v0.9") + implementation("com.google.android.flexbox:flexbox:3.0.0") implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b335ff9..a99d564 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,18 @@ android:name=".StatisticsActivity" android:label="@string/statistics_title" android:theme="@style/Theme.LunaTracker"/> + + + \ No newline at end of file diff --git a/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt new file mode 100644 index 0000000..c30b80f --- /dev/null +++ b/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt @@ -0,0 +1,177 @@ +package it.danieleverducci.lunatracker + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.progressindicator.LinearProgressIndicator +import it.danieleverducci.lunatracker.entities.Logbook +import it.danieleverducci.lunatracker.entities.LunaEvent +import it.danieleverducci.lunatracker.repository.FileLogbookRepository +import org.json.JSONArray +import org.json.JSONObject + +/** + * Activity for backup functionality (export/import logbooks). + */ +class BackupActivity : AppCompatActivity() { + + private lateinit var progressIndicator: LinearProgressIndicator + + private val exportLauncher = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { uri -> uri?.let { exportLogbookToUri(it) } } + + private val importLauncher = registerForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> uri?.let { importLogbookFromUri(it) } } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_backup) + + progressIndicator = findViewById(R.id.progress_indicator) + + findViewById(R.id.backup_export).setOnClickListener { + startExport() + } + findViewById(R.id.backup_import).setOnClickListener { + startImport() + } + findViewById(R.id.backup_close).setOnClickListener { + finish() + } + } + + private fun startExport() { + val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US) + .format(java.util.Date()) + exportLauncher.launch("lunatracker_backup_$timestamp.json") + } + + private fun startImport() { + importLauncher.launch(arrayOf("application/json")) + } + + private fun exportLogbookToUri(uri: Uri) { + progressIndicator.visibility = View.VISIBLE + Thread { + try { + val fileLogbookRepo = FileLogbookRepository() + val logbooks = fileLogbookRepo.getAllLogbooks(this) + + val json = JSONObject().apply { + put("version", 1) + put("app", "LunaTracker") + put("exported_at", System.currentTimeMillis()) + put("logbooks", JSONArray().apply { + logbooks.forEach { logbook -> + put(JSONObject().apply { + put("name", logbook.name) + put("events", JSONArray().apply { + logbook.logs.forEach { event -> + put(event.toJson()) + } + }) + }) + } + }) + } + + contentResolver.openOutputStream(uri)?.use { outputStream -> + outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8)) + } + + val eventCount = logbooks.sumOf { it.logs.size } + runOnUiThread { + progressIndicator.visibility = View.INVISIBLE + Toast.makeText( + this, + getString(R.string.export_success, eventCount), + Toast.LENGTH_SHORT + ).show() + } + } catch (e: Exception) { + runOnUiThread { + progressIndicator.visibility = View.INVISIBLE + Toast.makeText( + this, + getString(R.string.export_error) + e.message, + Toast.LENGTH_SHORT + ).show() + } + } + }.start() + } + + private fun importLogbookFromUri(uri: Uri) { + progressIndicator.visibility = View.VISIBLE + Thread { + try { + val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText() + ?: throw Exception("Could not read file") + + val json = JSONObject(jsonString) + + val fileLogbookRepo = FileLogbookRepository() + var totalEvents = 0 + + if (json.has("logbooks")) { + // New format with multiple logbooks + val logbooksArray = json.getJSONArray("logbooks") + for (i in 0 until logbooksArray.length()) { + val logbookJson = logbooksArray.getJSONObject(i) + val name = logbookJson.optString("name", "") + val eventsArray = logbookJson.getJSONArray("events") + + val logbook = Logbook(name) + for (j in 0 until eventsArray.length()) { + try { + logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j))) + totalEvents++ + } catch (e: IllegalArgumentException) { + // Skip invalid events + } + } + fileLogbookRepo.saveLogbook(this, logbook) + } + } else if (json.has("events")) { + // Old format with single logbook + val name = json.optString("logbook_name", "") + val eventsArray = json.getJSONArray("events") + + val logbook = Logbook(name) + for (i in 0 until eventsArray.length()) { + try { + logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i))) + totalEvents++ + } catch (e: IllegalArgumentException) { + // Skip invalid events + } + } + fileLogbookRepo.saveLogbook(this, logbook) + } + + runOnUiThread { + progressIndicator.visibility = View.INVISIBLE + Toast.makeText( + this, + getString(R.string.import_success, totalEvents), + Toast.LENGTH_SHORT + ).show() + } + } catch (e: Exception) { + runOnUiThread { + progressIndicator.visibility = View.INVISIBLE + Toast.makeText( + this, + getString(R.string.import_error) + ": " + e.message, + Toast.LENGTH_LONG + ).show() + } + } + }.start() + } +} diff --git a/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt new file mode 100644 index 0000000..eac7990 --- /dev/null +++ b/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt @@ -0,0 +1,71 @@ +package it.danieleverducci.lunatracker + +import android.os.Bundle +import android.widget.Button +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import it.danieleverducci.lunatracker.adapters.ButtonConfigAdapter +import it.danieleverducci.lunatracker.adapters.ButtonConfigItemTouchHelperCallback +import it.danieleverducci.lunatracker.repository.ButtonConfigRepository + +/** + * Activity for configuring which buttons appear on the main screen + * and in what order. + */ +class ButtonConfigActivity : AppCompatActivity() { + + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: ButtonConfigAdapter + private lateinit var itemTouchHelper: ItemTouchHelper + private lateinit var repository: ButtonConfigRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_button_config) + + repository = ButtonConfigRepository(this) + + setupRecyclerView() + setupButtons() + } + + private fun setupRecyclerView() { + recyclerView = findViewById(R.id.button_list) + recyclerView.layoutManager = LinearLayoutManager(this) + + // Load current configuration + val configs = repository.loadConfigs().toMutableList() + + adapter = ButtonConfigAdapter(configs) { viewHolder -> + itemTouchHelper.startDrag(viewHolder) + } + + recyclerView.adapter = adapter + + // Setup drag-and-drop + val callback = ButtonConfigItemTouchHelperCallback(adapter) + itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper.attachToRecyclerView(recyclerView) + } + + private fun setupButtons() { + findViewById