Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ab6b5f4b authored by mitulsheth's avatar mitulsheth
Browse files

feat: Murena Workspace Login Part 1 of 3

parent 423423da
Loading
Loading
Loading
Loading
Loading
+21 −2
Original line number Diff line number Diff line
@@ -28,8 +28,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import earth.maps.cardinal.data.DownloadStatusConverter

@Database(
    entities = [OfflineArea::class, RoutingProfile::class, DownloadedTile::class, SavedList::class, SavedPlace::class, ListItem::class, RecentSearch::class],
    version = 14,
    entities = [OfflineArea::class, RoutingProfile::class, DownloadedTile::class, SavedList::class, SavedPlace::class, ListItem::class, RecentSearch::class, FavoriteSyncRecord::class],
    version = 15,
    exportSchema = false
)
@TypeConverters(TileTypeConverter::class, DownloadStatusConverter::class, ItemTypeConverter::class)
@@ -41,6 +41,7 @@ abstract class AppDatabase : RoomDatabase() {
    abstract fun savedPlaceDao(): SavedPlaceDao
    abstract fun listItemDao(): ListItemDao
    abstract fun recentSearchDao(): RecentSearchDao
    abstract fun favoriteSyncRecordDao(): FavoriteSyncRecordDao

    companion object {
        @Volatile
@@ -243,6 +244,23 @@ abstract class AppDatabase : RoomDatabase() {
            }
        }

        private val MIGRATION_14_15 = object : Migration(14, 15) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL(
                    """
                    CREATE TABLE IF NOT EXISTS favorite_sync_records (
                        entityKey TEXT PRIMARY KEY NOT NULL,
                        entityType TEXT NOT NULL,
                        entityId TEXT NOT NULL,
                        revision TEXT,
                        isDirty INTEGER NOT NULL,
                        isDeleted INTEGER NOT NULL
                    )
                """.trimIndent()
                )
            }
        }

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
@@ -260,6 +278,7 @@ abstract class AppDatabase : RoomDatabase() {
                    MIGRATION_11_12,
                    MIGRATION_12_13,
                    MIGRATION_13_14,
                    MIGRATION_14_15,
                ).build()
                INSTANCE = instance
                instance
+33 −0
Original line number Diff line number Diff line
/*
 *     Cardinal Maps
 *     Copyright (C) 2026 Cardinal Maps Authors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package earth.maps.cardinal.data.room

object FavoriteSyncKeys {
    const val PLACE = "PLACE"
    const val LIST = "LIST"
    const val LIST_ITEM = "LIST_ITEM"

    fun place(id: String): String = "$PLACE:$id"

    fun list(id: String): String = "$LIST:$id"

    fun listItem(itemId: String, itemType: ItemType): String = "$LIST_ITEM:${itemType.name}:$itemId"

    fun listItemEntityId(itemId: String, itemType: ItemType): String = "${itemType.name}:$itemId"
}
+32 −0
Original line number Diff line number Diff line
/*
 *     Cardinal Maps
 *     Copyright (C) 2026 Cardinal Maps Authors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package earth.maps.cardinal.data.room

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "favorite_sync_records")
data class FavoriteSyncRecord(
    @PrimaryKey val entityKey: String,
    val entityType: String,
    val entityId: String,
    val revision: String?,
    val isDirty: Boolean,
    val isDeleted: Boolean
)
+42 −0
Original line number Diff line number Diff line
/*
 *     Cardinal Maps
 *     Copyright (C) 2026 Cardinal Maps Authors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package earth.maps.cardinal.data.room

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface FavoriteSyncRecordDao {
    @Query("SELECT * FROM favorite_sync_records")
    suspend fun getAllRecords(): List<FavoriteSyncRecord>

    @Query("SELECT * FROM favorite_sync_records WHERE entityKey = :entityKey")
    suspend fun getRecord(entityKey: String): FavoriteSyncRecord?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsertRecord(record: FavoriteSyncRecord)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsertRecords(records: List<FavoriteSyncRecord>)

    @Query("DELETE FROM favorite_sync_records WHERE isDeleted = 1")
    suspend fun deleteTombstoneRecords()
}
+71 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ class SavedListRepository @Inject constructor(
    private val listDao = database.savedListDao()
    private val listItemDao = database.listItemDao()
    private val placeDao = database.savedPlaceDao()
    private val favoriteSyncRecordDao = database.favoriteSyncRecordDao()

    /**
     * Creates a new list.
@@ -64,6 +65,7 @@ class SavedListRepository @Inject constructor(
            )

            listDao.insertList(list)
            markListDirty(id)

            val position = listItemDao.getItemsInList(parentId).size
            listItemDao.insertItem(
@@ -75,6 +77,7 @@ class SavedListRepository @Inject constructor(
                    addedAt = System.currentTimeMillis()
                )
            )
            markListItemDirty(id, ItemType.LIST)
            Result.success(id)
        } catch (e: Exception) {
            Result.failure(e)
@@ -90,6 +93,12 @@ class SavedListRepository @Inject constructor(
                IllegalArgumentException("List not found")
            )

            markDeleted(FavoriteSyncKeys.list(listId), FavoriteSyncKeys.LIST, listId)
            markDeleted(
                FavoriteSyncKeys.listItem(listId, ItemType.LIST),
                FavoriteSyncKeys.LIST_ITEM,
                FavoriteSyncKeys.listItemEntityId(listId, ItemType.LIST)
            )
            listItemDao.orphanItem(listId, ItemType.LIST)
            listDao.deleteList(list)
            Result.success(Unit)
@@ -118,6 +127,7 @@ class SavedListRepository @Inject constructor(
            )

            listDao.updateList(updatedList)
            markListDirty(listId)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
@@ -208,6 +218,7 @@ class SavedListRepository @Inject constructor(
            )

            listItemDao.insertItem(listItem)
            markListItemDirty(itemId, itemType)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
@@ -222,6 +233,22 @@ class SavedListRepository @Inject constructor(
    ): Result<Unit> = withContext(Dispatchers.IO) {
        try {
            listItemDao.reorderItems(listId, items)
            items.forEach { markListItemDirty(it.itemId, it.itemType) }
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun moveItem(
        itemId: String,
        itemType: ItemType,
        newListId: String,
        newPosition: Int
    ): Result<Unit> = withContext(Dispatchers.IO) {
        try {
            listItemDao.moveItem(itemId, newListId, newPosition)
            markListItemDirty(itemId, itemType)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
@@ -249,6 +276,50 @@ class SavedListRepository @Inject constructor(
            items.map { it.itemId }.toSet()
        }

    private suspend fun markListDirty(listId: String) {
        val key = FavoriteSyncKeys.list(listId)
        val existing = favoriteSyncRecordDao.getRecord(key)
        favoriteSyncRecordDao.upsertRecord(
            FavoriteSyncRecord(
                entityKey = key,
                entityType = FavoriteSyncKeys.LIST,
                entityId = listId,
                revision = existing?.revision,
                isDirty = true,
                isDeleted = false
            )
        )
    }

    private suspend fun markListItemDirty(itemId: String, itemType: ItemType) {
        val key = FavoriteSyncKeys.listItem(itemId, itemType)
        val existing = favoriteSyncRecordDao.getRecord(key)
        favoriteSyncRecordDao.upsertRecord(
            FavoriteSyncRecord(
                entityKey = key,
                entityType = FavoriteSyncKeys.LIST_ITEM,
                entityId = FavoriteSyncKeys.listItemEntityId(itemId, itemType),
                revision = existing?.revision,
                isDirty = true,
                isDeleted = false
            )
        )
    }

    private suspend fun markDeleted(entityKey: String, entityType: String, entityId: String) {
        val existing = favoriteSyncRecordDao.getRecord(entityKey)
        favoriteSyncRecordDao.upsertRecord(
            FavoriteSyncRecord(
                entityKey = entityKey,
                entityType = entityType,
                entityId = entityId,
                revision = existing?.revision,
                isDirty = true,
                isDeleted = true
            )
        )
    }

    /**
     * Gets the hierarchical content of a list for UI display.
     * Returns a flow of list of ListContent items (either PlaceContent or ListContentItem).
Loading