Compare commits
1 Commits
v1.8.1
...
2-Migrate-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ddad7e5e7 |
@@ -4,6 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
|
alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
|
||||||
alias(libs.plugins.ktlint) // ✅ v1.6.1: Reaktiviert nach Code-Cleanup
|
alias(libs.plugins.ktlint) // ✅ v1.6.1: Reaktiviert nach Code-Cleanup
|
||||||
alias(libs.plugins.detekt)
|
alias(libs.plugins.detekt)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
@@ -162,6 +163,15 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
|
||||||
|
// Koin
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.koin.androidx.compose)
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 🆕 v1.8.0: Homescreen Widgets
|
// 🆕 v1.8.0: Homescreen Widgets
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import android.widget.CheckBox
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import dev.dettmer.simplenotes.utils.Logger
|
|||||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
|
import dev.dettmer.simplenotes.di.appModule
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
class SimpleNotesApplication : Application() {
|
class SimpleNotesApplication : Application() {
|
||||||
|
|
||||||
@@ -30,6 +34,12 @@ class SimpleNotesApplication : Application() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidLogger() // Log Koin events
|
||||||
|
androidContext(this@SimpleNotesApplication) // Provide context to modules
|
||||||
|
modules(appModule)
|
||||||
|
}
|
||||||
|
|
||||||
val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
// 🔧 Hotfix v1.6.2: Migrate offline mode setting BEFORE any ViewModel initialization
|
// 🔧 Hotfix v1.6.2: Migrate offline mode setting BEFORE any ViewModel initialization
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.dettmer.simplenotes.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import dev.dettmer.simplenotes.storage.AppDatabase
|
||||||
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
|
import dev.dettmer.simplenotes.ui.main.MainViewModel
|
||||||
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val appModule = module {
|
||||||
|
single {
|
||||||
|
Room.databaseBuilder(
|
||||||
|
androidContext(),
|
||||||
|
AppDatabase::class.java,
|
||||||
|
"notes_database"
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
single { get<AppDatabase>().noteDao() }
|
||||||
|
single { get<AppDatabase>().deletedNoteDao() }
|
||||||
|
|
||||||
|
// Provide SharedPreferences
|
||||||
|
single {
|
||||||
|
androidContext().getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
single { NotesStorage(androidContext(), get()) }
|
||||||
|
|
||||||
|
viewModel { MainViewModel(get(), get()) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.dettmer.simplenotes.storage
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import dev.dettmer.simplenotes.storage.dao.DeletedNoteDao
|
||||||
|
import dev.dettmer.simplenotes.storage.dao.NoteDao
|
||||||
|
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||||
|
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||||
|
|
||||||
|
@Database(entities = [NoteEntity::class, DeletedNoteEntity::class], version = 1)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun noteDao(): NoteDao
|
||||||
|
abstract fun deletedNoteDao(): DeletedNoteDao
|
||||||
|
}
|
||||||
@@ -1,76 +1,58 @@
|
|||||||
package dev.dettmer.simplenotes.storage
|
package dev.dettmer.simplenotes.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.dettmer.simplenotes.models.DeletionTracker
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.models.Note
|
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||||
|
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||||
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class NotesStorage(private val context: Context) {
|
class NotesStorage(
|
||||||
|
private val context: Context,
|
||||||
|
database: AppDatabase
|
||||||
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NotesStorage"
|
private const val TAG = "NotesStorage"
|
||||||
// 🔒 v1.7.2 (IMPL_001): Mutex für thread-sichere Deletion Tracker Operationen
|
|
||||||
private val deletionTrackerMutex = Mutex()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val notesDir: File = File(context.filesDir, "notes").apply {
|
private val noteDao = database.noteDao()
|
||||||
if (!exists()) mkdirs()
|
private val deletedNoteDao = database.deletedNoteDao()
|
||||||
|
|
||||||
|
suspend fun saveNote(note: NoteEntity) {
|
||||||
|
noteDao.saveNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveNote(note: Note) {
|
suspend fun loadNote(id: String): NoteEntity? {
|
||||||
val file = File(notesDir, "${note.id}.json")
|
return noteDao.getNote(id)
|
||||||
file.writeText(note.toJson())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadNote(id: String): Note? {
|
suspend fun loadAllNotes(): List<NoteEntity> {
|
||||||
val file = File(notesDir, "$id.json")
|
return noteDao.getAllNotes()
|
||||||
return if (file.exists()) {
|
|
||||||
Note.fromJson(file.readText())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun deleteNote(id: String): Boolean {
|
||||||
* Lädt alle Notizen aus dem lokalen Speicher.
|
val deletedRows = noteDao.deleteNoteById(id)
|
||||||
*
|
|
||||||
* 🔀 v1.8.0: Sortierung entfernt — wird jetzt im ViewModel durchgeführt,
|
|
||||||
* damit der User die Sortierung konfigurieren kann.
|
|
||||||
*/
|
|
||||||
fun loadAllNotes(): List<Note> {
|
|
||||||
return notesDir.listFiles()
|
|
||||||
?.filter { it.extension == "json" }
|
|
||||||
?.mapNotNull { Note.fromJson(it.readText()) }
|
|
||||||
?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteNote(id: String): Boolean {
|
if (deletedRows > 0) {
|
||||||
val file = File(notesDir, "$id.json")
|
|
||||||
val deleted = file.delete()
|
|
||||||
|
|
||||||
if (deleted) {
|
|
||||||
Logger.d(TAG, "🗑️ Deleted note: $id")
|
Logger.d(TAG, "🗑️ Deleted note: $id")
|
||||||
|
|
||||||
// Track deletion to prevent zombie notes
|
|
||||||
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
||||||
trackDeletion(id, deviceId)
|
trackDeletionSafe(id, deviceId)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return deleted
|
suspend fun deleteAllNotes(): Boolean {
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAllNotes(): Boolean {
|
|
||||||
return try {
|
return try {
|
||||||
val notes = loadAllNotes()
|
val notes = loadAllNotes()
|
||||||
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
||||||
|
|
||||||
for (note in notes) {
|
// Batch tracking and deleting
|
||||||
deleteNote(note.id) // Uses trackDeletion() automatically
|
notes.forEach { note ->
|
||||||
|
trackDeletionSafe(note.id, deviceId)
|
||||||
}
|
}
|
||||||
|
noteDao.deleteAllNotes()
|
||||||
|
|
||||||
Logger.d(TAG, "🗑️ Deleted all notes (${notes.size} notes)")
|
Logger.d(TAG, "🗑️ Deleted all notes (${notes.size} notes)")
|
||||||
true
|
true
|
||||||
@@ -82,101 +64,28 @@ class NotesStorage(private val context: Context) {
|
|||||||
|
|
||||||
// === Deletion Tracking ===
|
// === Deletion Tracking ===
|
||||||
|
|
||||||
private fun getDeletionTrackerFile(): File {
|
|
||||||
return File(context.filesDir, "deleted_notes.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadDeletionTracker(): DeletionTracker {
|
|
||||||
val file = getDeletionTrackerFile()
|
|
||||||
if (!file.exists()) {
|
|
||||||
return DeletionTracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val json = file.readText()
|
|
||||||
DeletionTracker.fromJson(json) ?: DeletionTracker()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to load deletion tracker", e)
|
|
||||||
DeletionTracker()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveDeletionTracker(tracker: DeletionTracker) {
|
|
||||||
try {
|
|
||||||
val file = getDeletionTrackerFile()
|
|
||||||
file.writeText(tracker.toJson())
|
|
||||||
|
|
||||||
if (tracker.deletedNotes.size > 1000) {
|
|
||||||
Logger.w(TAG, "⚠️ Deletion tracker large: ${tracker.deletedNotes.size} entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d(TAG, "✅ Deletion tracker saved (${tracker.deletedNotes.size} entries)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to save deletion tracker", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🔒 v1.7.2 (IMPL_001): Thread-sichere Deletion-Tracking mit Mutex
|
|
||||||
*
|
|
||||||
* Verhindert Race Conditions bei Batch-Deletes durch exklusiven Zugriff
|
|
||||||
* auf den Deletion Tracker.
|
|
||||||
*
|
|
||||||
* @param noteId ID der gelöschten Notiz
|
|
||||||
* @param deviceId Geräte-ID für Konflikt-Erkennung
|
|
||||||
*/
|
|
||||||
suspend fun trackDeletionSafe(noteId: String, deviceId: String) {
|
suspend fun trackDeletionSafe(noteId: String, deviceId: String) {
|
||||||
deletionTrackerMutex.withLock {
|
// Room handles internal transactions and thread-safety natively.
|
||||||
val tracker = loadDeletionTracker()
|
// The Mutex is no longer required.
|
||||||
tracker.addDeletion(noteId, deviceId)
|
deletedNoteDao.trackDeletion(DeletedNoteEntity(noteId, deviceId))
|
||||||
saveDeletionTracker(tracker)
|
|
||||||
Logger.d(TAG, "📝 Tracked deletion (mutex-protected): $noteId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy-Methode ohne Mutex-Schutz.
|
|
||||||
* Verwendet für synchrone Aufrufe wo Coroutines nicht verfügbar sind.
|
|
||||||
*
|
|
||||||
* @deprecated Verwende trackDeletionSafe() für Thread-Safety wo möglich
|
|
||||||
*/
|
|
||||||
fun trackDeletion(noteId: String, deviceId: String) {
|
|
||||||
val tracker = loadDeletionTracker()
|
|
||||||
tracker.addDeletion(noteId, deviceId)
|
|
||||||
saveDeletionTracker(tracker)
|
|
||||||
Logger.d(TAG, "📝 Tracked deletion: $noteId")
|
Logger.d(TAG, "📝 Tracked deletion: $noteId")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNoteDeleted(noteId: String): Boolean {
|
suspend fun isNoteDeleted(noteId: String): Boolean {
|
||||||
val tracker = loadDeletionTracker()
|
return deletedNoteDao.isNoteDeleted(noteId)
|
||||||
return tracker.isDeleted(noteId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearDeletionTracker() {
|
suspend fun clearDeletionTracker() {
|
||||||
saveDeletionTracker(DeletionTracker())
|
deletedNoteDao.clearTracker()
|
||||||
Logger.d(TAG, "🗑️ Deletion tracker cleared")
|
Logger.d(TAG, "🗑️ Deletion tracker cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun resetAllSyncStatusToPending(): Int {
|
||||||
* 🔄 v1.7.0: Reset all sync statuses to PENDING when server changes
|
val updatedCount = noteDao.updateSyncStatus(
|
||||||
* This ensures notes are uploaded to the new server on next sync
|
oldStatus = SyncStatus.SYNCED,
|
||||||
*/
|
newStatus = SyncStatus.PENDING
|
||||||
fun resetAllSyncStatusToPending(): Int {
|
)
|
||||||
val notes = loadAllNotes()
|
|
||||||
var updatedCount = 0
|
|
||||||
|
|
||||||
notes.forEach { note ->
|
|
||||||
if (note.syncStatus == dev.dettmer.simplenotes.models.SyncStatus.SYNCED) {
|
|
||||||
val updatedNote = note.copy(syncStatus = dev.dettmer.simplenotes.models.SyncStatus.PENDING)
|
|
||||||
saveNote(updatedNote)
|
|
||||||
updatedCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d(TAG, "🔄 Reset sync status for $updatedCount notes to PENDING")
|
Logger.d(TAG, "🔄 Reset sync status for $updatedCount notes to PENDING")
|
||||||
return updatedCount
|
return updatedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getNotesDir(): File = notesDir
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.dettmer.simplenotes.storage.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DeletedNoteDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun trackDeletion(deletedNote: DeletedNoteEntity)
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT 1 FROM deleted_notes WHERE noteId = :noteId)")
|
||||||
|
suspend fun isNoteDeleted(noteId: String): Boolean
|
||||||
|
|
||||||
|
@Query("DELETE FROM deleted_notes")
|
||||||
|
suspend fun clearTracker()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.dettmer.simplenotes.storage.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
|
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface NoteDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun saveNote(note: NoteEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM notes WHERE id = :id")
|
||||||
|
suspend fun getNote(id: String): NoteEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM notes")
|
||||||
|
suspend fun getAllNotes(): List<NoteEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM notes WHERE id = :id")
|
||||||
|
suspend fun deleteNoteById(id: String): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM notes")
|
||||||
|
suspend fun deleteAllNotes(): Int
|
||||||
|
|
||||||
|
@Query("UPDATE notes SET syncStatus = :newStatus WHERE syncStatus = :oldStatus")
|
||||||
|
suspend fun updateSyncStatus(oldStatus: SyncStatus, newStatus: SyncStatus): Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.dettmer.simplenotes.storage.entity
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "deleted_notes")
|
||||||
|
data class DeletedNoteEntity(
|
||||||
|
@PrimaryKey val noteId: String,
|
||||||
|
val deviceId: String,
|
||||||
|
val deletedAt: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.dettmer.simplenotes.storage.entity
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
|
|
||||||
|
@Entity(tableName = "notes")
|
||||||
|
data class NoteEntity(
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val content: String,
|
||||||
|
val timestamp: Long,
|
||||||
|
val syncStatus: SyncStatus
|
||||||
|
)
|
||||||
@@ -44,6 +44,7 @@ import dev.dettmer.simplenotes.utils.Constants
|
|||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Activity with Jetpack Compose UI
|
* Main Activity with Jetpack Compose UI
|
||||||
@@ -65,7 +66,7 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
private const val REQUEST_SETTINGS = 1002
|
private const val REQUEST_SETTINGS = 1002
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModel()
|
||||||
|
|
||||||
private val prefs by lazy {
|
private val prefs by lazy {
|
||||||
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import dev.dettmer.simplenotes.ui.main.components.NotesStaggeredGrid
|
|||||||
import dev.dettmer.simplenotes.ui.main.components.SyncProgressBanner
|
import dev.dettmer.simplenotes.ui.main.components.SyncProgressBanner
|
||||||
import dev.dettmer.simplenotes.ui.main.components.SyncStatusLegendDialog
|
import dev.dettmer.simplenotes.ui.main.components.SyncStatusLegendDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L
|
private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel,
|
viewModel: MainViewModel = koinViewModel(),
|
||||||
onOpenNote: (String?) -> Unit,
|
onOpenNote: (String?) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onCreateNote: (NoteType) -> Unit
|
onCreateNote: (NoteType) -> Unit
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package dev.dettmer.simplenotes.ui.main
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.dettmer.simplenotes.models.Note
|
import dev.dettmer.simplenotes.models.Note
|
||||||
import dev.dettmer.simplenotes.models.SortDirection
|
import dev.dettmer.simplenotes.models.SortDirection
|
||||||
@@ -34,7 +36,10 @@ import kotlinx.coroutines.withContext
|
|||||||
*
|
*
|
||||||
* Manages notes list, sync state, and deletion with undo.
|
* Manages notes list, sync state, and deletion with undo.
|
||||||
*/
|
*/
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
class MainViewModel(
|
||||||
|
private val storage: NotesStorage,
|
||||||
|
private val prefs: SharedPreferences
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainViewModel"
|
private const val TAG = "MainViewModel"
|
||||||
@@ -42,9 +47,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val storage = NotesStorage(application)
|
|
||||||
private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Notes State
|
// Notes State
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -207,7 +209,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private suspend fun loadNotesAsync() {
|
private suspend fun loadNotesAsync() {
|
||||||
val allNotes = storage.loadAllNotes()
|
val allNotes = storage.loadAllNotes()
|
||||||
val pendingIds = _pendingDeletions.value
|
val pendingIds = _pendingDeletions.value
|
||||||
val filteredNotes = allNotes.filter { it.id !in pendingIds }
|
val filteredNotes = allNotes.filter { it.id !in pendingIds }.map { Note(
|
||||||
|
id = it.id,
|
||||||
|
content = it.content
|
||||||
|
) }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// Phase 3: Detect if a new note was added at the top
|
// Phase 3: Detect if a new note was added at the top
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package dev.dettmer.simplenotes
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,9 @@ composeBom = "2026.01.00"
|
|||||||
navigationCompose = "2.7.6"
|
navigationCompose = "2.7.6"
|
||||||
lifecycleRuntimeCompose = "2.7.0"
|
lifecycleRuntimeCompose = "2.7.0"
|
||||||
activityCompose = "1.8.2"
|
activityCompose = "1.8.2"
|
||||||
|
room = "2.6.1"
|
||||||
|
ksp = "2.0.0-1.0.21"
|
||||||
|
koin = "3.5.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -37,6 +40,14 @@ androidx-compose-material-icons = { group = "androidx.compose.material", name =
|
|||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
||||||
|
# Room Database
|
||||||
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
|
# Core Koin for Kotlin projects
|
||||||
|
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||||
|
# Koin for Jetpack Compose integration
|
||||||
|
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
@@ -44,4 +55,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
||||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user