From ea53c325b0e6491d69dc3bef2f715da417209fdc Mon Sep 17 00:00:00 2001 From: hegocre <15657088+hegocre@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:07:11 +0100 Subject: [PATCH] Add password sharing --- .../4.json | 386 ++++++++++++++++++ .../nextcloudpasswords/api/ApiController.kt | 7 + .../nextcloudpasswords/api/ShareApi.kt | 78 ++++ .../nextcloudpasswords/data/share/Share.kt | 23 ++ .../data/share/ShareController.kt | 53 +++ .../data/share/ShareUser.kt | 10 + .../databases/AppDatabase.kt | 9 +- .../databases/Converters.kt | 14 + .../sharedatabase/ShareDatabaseDao.kt | 30 ++ .../ui/components/NCPApp.kt | 7 + .../ui/components/NCPNavHost.kt | 1 + .../ui/components/PasswordItem.kt | 26 +- .../ui/viewmodels/PasswordsViewModel.kt | 5 + 13 files changed, 646 insertions(+), 3 deletions(-) create mode 100644 app/schemas/com.hegocre.nextcloudpasswords.databases.AppDatabase/4.json create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/api/ShareApi.kt create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/data/share/Share.kt create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareController.kt create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareUser.kt create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/databases/Converters.kt create mode 100644 app/src/main/java/com/hegocre/nextcloudpasswords/databases/sharedatabase/ShareDatabaseDao.kt diff --git a/app/schemas/com.hegocre.nextcloudpasswords.databases.AppDatabase/4.json b/app/schemas/com.hegocre.nextcloudpasswords.databases.AppDatabase/4.json new file mode 100644 index 00000000..5a14eca0 --- /dev/null +++ b/app/schemas/com.hegocre.nextcloudpasswords.databases.AppDatabase/4.json @@ -0,0 +1,386 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "7010d0dd6ee9602bbd8341f6f5e1faa2", + "entities": [ + { + "tableName": "folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT NOT NULL, `parent` TEXT NOT NULL, `revision` TEXT NOT NULL, `cseType` TEXT NOT NULL, `cseKey` TEXT NOT NULL, `sseType` TEXT NOT NULL, `client` TEXT NOT NULL, `hidden` INTEGER NOT NULL, `trashed` INTEGER NOT NULL, `favorite` INTEGER NOT NULL, `created` INTEGER NOT NULL, `updated` INTEGER NOT NULL, `edited` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cseType", + "columnName": "cseType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cseKey", + "columnName": "cseKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sseType", + "columnName": "sseType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "client", + "columnName": "client", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashed", + "columnName": "trashed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "edited", + "columnName": "edited", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_folders_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_folders_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "passwords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `url` TEXT NOT NULL, `notes` TEXT NOT NULL, `customFields` TEXT NOT NULL, `status` INTEGER NOT NULL, `statusCode` TEXT NOT NULL, `hash` TEXT NOT NULL, `folder` TEXT NOT NULL, `revision` TEXT NOT NULL, `share` TEXT, `shared` INTEGER NOT NULL, `cseType` TEXT NOT NULL, `cseKey` TEXT NOT NULL, `sseType` TEXT NOT NULL, `client` TEXT NOT NULL, `hidden` INTEGER NOT NULL, `trashed` INTEGER NOT NULL, `favorite` INTEGER NOT NULL, `editable` INTEGER NOT NULL, `edited` INTEGER NOT NULL, `created` INTEGER NOT NULL, `updated` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "customFields", + "columnName": "customFields", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "statusCode", + "columnName": "statusCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "share", + "columnName": "share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shared", + "columnName": "shared", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cseType", + "columnName": "cseType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cseKey", + "columnName": "cseKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sseType", + "columnName": "sseType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "client", + "columnName": "client", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashed", + "columnName": "trashed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editable", + "columnName": "editable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "edited", + "columnName": "edited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_passwords_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_passwords_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "shares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `created` INTEGER NOT NULL, `updated` INTEGER NOT NULL, `expires` INTEGER, `editable` INTEGER NOT NULL, `shareable` INTEGER NOT NULL, `updatePending` INTEGER NOT NULL, `password` TEXT NOT NULL, `owner` TEXT NOT NULL, `receiver` TEXT NOT NULL, `client` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expires", + "columnName": "expires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "editable", + "columnName": "editable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareable", + "columnName": "shareable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatePending", + "columnName": "updatePending", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receiver", + "columnName": "receiver", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "client", + "columnName": "client", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_shares_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_shares_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7010d0dd6ee9602bbd8341f6f5e1faa2')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt index f2f33d6a..1d9f63bd 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt @@ -15,6 +15,7 @@ import com.hegocre.nextcloudpasswords.data.password.DeletedPassword import com.hegocre.nextcloudpasswords.data.password.NewPassword import com.hegocre.nextcloudpasswords.data.password.Password import com.hegocre.nextcloudpasswords.data.password.UpdatedPassword +import com.hegocre.nextcloudpasswords.data.share.Share import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.services.keepalive.KeepAliveWorker import com.hegocre.nextcloudpasswords.utils.Error @@ -46,6 +47,7 @@ class ApiController private constructor(context: Context) { private val sessionApi = SessionApi.getInstance(server) private val serviceApi = ServiceApi.getInstance(server) private val settingsApi = SettingsApi.getInstance(server) + private val shareApi = ShareApi.getInstance(server) private var sessionCode: String? = null @@ -256,6 +258,11 @@ class ApiController private constructor(context: Context) { return foldersApi.list(sessionCode) } + suspend fun listShares(): Result> { + if (!sessionOpen.value) return Result.Error(Error.API_NO_SESSION) + return shareApi.list(sessionCode) + } + /** * Creates a new password via the [PasswordsApi] class. This can only be called when a * session is open, otherwise an error is thrown. diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ShareApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ShareApi.kt new file mode 100644 index 00000000..89a7a4a3 --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ShareApi.kt @@ -0,0 +1,78 @@ +package com.hegocre.nextcloudpasswords.api + +import com.hegocre.nextcloudpasswords.BuildConfig +import com.hegocre.nextcloudpasswords.data.share.Share +import com.hegocre.nextcloudpasswords.utils.Error +import com.hegocre.nextcloudpasswords.utils.OkHttpRequest +import com.hegocre.nextcloudpasswords.utils.Result +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.net.SocketTimeoutException +import javax.net.ssl.SSLHandshakeException + +class ShareApi private constructor(private var server: Server) { + + suspend fun list( + sessionCode: String? = null, + ): Result> { + return try { + val apiResponse = withContext(Dispatchers.IO) { + OkHttpRequest.getInstance().get( + sUrl = server.url + LIST_URL, + sessionCode = sessionCode, + username = server.username, + password = server.password + ) + } + + val code = apiResponse.code + val body = withContext(Dispatchers.IO) { apiResponse.body?.string() } + withContext(Dispatchers.IO) { + apiResponse.close() + } + + if (code != 200 || body == null) { + return Result.Error(Error.API_BAD_RESPONSE) + } + + withContext(Dispatchers.Default) { + Result.Success(Json.decodeFromString(body)) + } + } catch (e: SocketTimeoutException) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + Result.Error(Error.API_TIMEOUT) + } catch (e: SSLHandshakeException) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + Result.Error(Error.SSL_HANDSHAKE_EXCEPTION) + } catch (e: Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + Result.Error(Error.UNKNOWN) + } + } + + companion object { + private const val LIST_URL = "/index.php/apps/passwords/api/1.0/share/list" + + private var instance: ShareApi? = null + + fun getInstance(server: Server): ShareApi { + synchronized(this) { + var tempInstance = instance + + if (tempInstance == null) { + tempInstance = ShareApi(server) + instance = tempInstance + } + + return tempInstance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/Share.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/Share.kt new file mode 100644 index 00000000..3065bade --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/Share.kt @@ -0,0 +1,23 @@ +package com.hegocre.nextcloudpasswords.data.share + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "shares", indices = [Index(value = ["id"], unique = true)]) +data class Share( + @PrimaryKey + val id: String, + val created: Int, + val updated: Int, + val expires: Int?, + val editable: Boolean, + val shareable: Boolean, + val updatePending: Boolean, + val password: String, + val owner: ShareUser, + val receiver: ShareUser, + val client: String +) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareController.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareController.kt new file mode 100644 index 00000000..0a1897c1 --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareController.kt @@ -0,0 +1,53 @@ +package com.hegocre.nextcloudpasswords.data.share + +import android.content.Context +import androidx.lifecycle.LiveData +import com.hegocre.nextcloudpasswords.api.ApiController +import com.hegocre.nextcloudpasswords.databases.AppDatabase +import com.hegocre.nextcloudpasswords.utils.Result +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ShareController private constructor(context: Context) { + private val passwordDatabase = AppDatabase.getInstance(context) + private val apiController = ApiController.getInstance(context) + + suspend fun syncShares() { + withContext(Dispatchers.IO) { + val result = apiController.listShares() + if (result is Result.Success) { + val savedSharesSet = passwordDatabase.shareDao.fetchAllSharesId().toHashSet() + for (share in result.data) { + val oldUpdated = passwordDatabase.shareDao.getShareUpdated(share.id) + if (oldUpdated == null || oldUpdated != share.updated) { + passwordDatabase.shareDao.insertShare(share) + } + savedSharesSet.remove(share.id) + } + for (id in savedSharesSet) { + passwordDatabase.shareDao.deleteShare(id) + } + } + } + } + + fun getShares(): LiveData> = + passwordDatabase.shareDao.fetchAllShares() + + companion object { + private var instance: ShareController? = null + + fun getInstance(context: Context): ShareController { + synchronized(this) { + var tempInstance = instance + + if (tempInstance == null) { + tempInstance = ShareController(context) + instance = tempInstance + } + + return tempInstance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareUser.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareUser.kt new file mode 100644 index 00000000..6306f64c --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/data/share/ShareUser.kt @@ -0,0 +1,10 @@ +package com.hegocre.nextcloudpasswords.data.share + +import kotlinx.serialization.Serializable + +@Serializable +data class ShareUser( + val id: String, + val name: String +) + diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/databases/AppDatabase.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/AppDatabase.kt index a3669ed9..78c14bd1 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/databases/AppDatabase.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/AppDatabase.kt @@ -6,22 +6,27 @@ import androidx.room.Database import androidx.room.DeleteTable import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import androidx.room.migration.AutoMigrationSpec import com.hegocre.nextcloudpasswords.data.folder.Folder import com.hegocre.nextcloudpasswords.data.password.Password +import com.hegocre.nextcloudpasswords.data.share.Share import com.hegocre.nextcloudpasswords.databases.folderdatabase.FolderDatabaseDao import com.hegocre.nextcloudpasswords.databases.passworddatabase.PasswordDatabaseDao +import com.hegocre.nextcloudpasswords.databases.sharedatabase.ShareDatabaseDao @Database( - entities = [Folder::class, Password::class], - version = 3, + entities = [Folder::class, Password::class, Share::class], + version = 4, autoMigrations = [ AutoMigration(from = 2, to = 3, spec = AppDatabase.DeleteFaviconsMigration::class) ] ) +@TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract val passwordDao: PasswordDatabaseDao abstract val folderDao: FolderDatabaseDao + abstract val shareDao: ShareDatabaseDao @DeleteTable(tableName = "favicons") class DeleteFaviconsMigration : AutoMigrationSpec diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/databases/Converters.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/Converters.kt new file mode 100644 index 00000000..6c1ddd46 --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/Converters.kt @@ -0,0 +1,14 @@ +package com.hegocre.nextcloudpasswords.databases + +import androidx.room.TypeConverter +import com.hegocre.nextcloudpasswords.data.share.ShareUser +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class Converters { + @TypeConverter + fun shareUserToString(shareUser: ShareUser): String = Json.encodeToString(shareUser) + + @TypeConverter + fun shareUserFromString(shareUser: String): ShareUser = Json.decodeFromString(shareUser) +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/databases/sharedatabase/ShareDatabaseDao.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/sharedatabase/ShareDatabaseDao.kt new file mode 100644 index 00000000..5b1129fc --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/databases/sharedatabase/ShareDatabaseDao.kt @@ -0,0 +1,30 @@ +package com.hegocre.nextcloudpasswords.databases.sharedatabase + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.hegocre.nextcloudpasswords.data.share.Share + +@Dao +interface ShareDatabaseDao { + @Query("SELECT * FROM shares") + fun fetchAllShares(): LiveData> + + @Query("SELECT id FROM shares") + suspend fun fetchAllSharesId(): List + + @Query("SELECT updated FROM shares WHERE id = :id") + suspend fun getShareUpdated(id: String): Int? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertShare(share: Share) + + @Delete + suspend fun deleteShare(share: Share) + + @Query("DELETE FROM shares WHERE id = :id") + suspend fun deleteShare(id: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt index 7f9994d6..ff3c2fa1 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -285,7 +286,12 @@ fun NextcloudPasswordsApp( ) } + val shares by passwordsViewModel.shares.observeAsState() if (openBottomSheet) { + val shareInfo = remember (shares, passwordsViewModel.visiblePassword.value) { + shares?.find { it.password == passwordsViewModel.visiblePassword.value?.first?.id } + } + ModalBottomSheet( onDismissRequest = { openBottomSheet = false }, contentWindowInsets = { WindowInsets.navigationBars }, @@ -293,6 +299,7 @@ fun NextcloudPasswordsApp( ) { PasswordItem( passwordInfo = passwordsViewModel.visiblePassword.value, + shareInfo = shareInfo, onEditPassword = if (sessionOpen) { { coroutineScope.launch { diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPNavHost.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPNavHost.kt index 65fa33b0..71b44341 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPNavHost.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPNavHost.kt @@ -77,6 +77,7 @@ fun NCPNavHost( val passwords by passwordsViewModel.passwords.observeAsState() val folders by passwordsViewModel.folders.observeAsState() + val shares by passwordsViewModel.shares.observeAsState() val keychain by passwordsViewModel.csEv1Keychain.observeAsState() val isRefreshing by passwordsViewModel.isRefreshing.collectAsState() val isUpdating by passwordsViewModel.isUpdating.collectAsState() diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/PasswordItem.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/PasswordItem.kt index d984818b..f8f1b2cd 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/PasswordItem.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/PasswordItem.kt @@ -55,6 +55,8 @@ import androidx.compose.ui.unit.dp import com.hegocre.nextcloudpasswords.R import com.hegocre.nextcloudpasswords.data.password.CustomField import com.hegocre.nextcloudpasswords.data.password.Password +import com.hegocre.nextcloudpasswords.data.share.Share +import com.hegocre.nextcloudpasswords.data.share.ShareUser import com.hegocre.nextcloudpasswords.ui.components.markdown.MDDocument import com.hegocre.nextcloudpasswords.ui.theme.ContentAlpha import com.hegocre.nextcloudpasswords.ui.theme.NextcloudPasswordsTheme @@ -67,12 +69,14 @@ import org.commonmark.parser.Parser @Composable fun PasswordItem( passwordInfo: Pair>?, + shareInfo: Share?, modifier: Modifier = Modifier, onEditPassword: (() -> Unit)? = null, ) { passwordInfo?.let { pass -> PasswordItemContent( passwordInfo = pass, + shareInfo = shareInfo, onEditPassword = onEditPassword, modifier = modifier ) @@ -86,6 +90,7 @@ fun PasswordItem( @Composable fun PasswordItemContent( passwordInfo: Pair>, + shareInfo: Share?, onEditPassword: (() -> Unit)?, modifier: Modifier = Modifier ) { @@ -200,6 +205,12 @@ fun PasswordItemContent( ) } + shareInfo?.let { shareInfo -> + item(key = "${password.id}_shareInfo") { + Text(text = "Shared by ${shareInfo.owner.name}") + } + } + if (password.username.isNotBlank()) { item(key = "${password.id}_username") { val usernameLabel = stringResource(id = R.string.password_attr_username) @@ -533,7 +544,7 @@ fun PasswordMarkdownField( } } -@Preview +@Preview(apiLevel = 34) @Composable fun PasswordItemPreview() { NextcloudPasswordsTheme { @@ -570,6 +581,19 @@ fun PasswordItemPreview() { updated = 0 ), listOf("Second", "Home") ), + shareInfo = Share( + id = "", + created = 0, + updated = 0, + expires = null, + editable = true, + shareable = false, + updatePending = false, + password = "", + owner = ShareUser("admin", "Admin"), + receiver = ShareUser("admin2", "Admin2"), + client = "" + ), onEditPassword = {}, modifier = Modifier.padding(bottom = 16.dp) ) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt index 23eac96e..51cafb04 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt @@ -37,6 +37,8 @@ import com.hegocre.nextcloudpasswords.data.password.Password import com.hegocre.nextcloudpasswords.data.password.PasswordController import com.hegocre.nextcloudpasswords.data.password.UpdatedPassword import com.hegocre.nextcloudpasswords.data.serversettings.ServerSettings +import com.hegocre.nextcloudpasswords.data.share.Share +import com.hegocre.nextcloudpasswords.data.share.ShareController import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.AppLockHelper import com.hegocre.nextcloudpasswords.utils.PreferencesManager @@ -100,6 +102,8 @@ class PasswordsViewModel(application: Application) : AndroidViewModel(applicatio get() = PasswordController.getInstance(getApplication()).getPasswords() val folders: LiveData> get() = FolderController.getInstance(getApplication()).getFolders() + val shares: LiveData> + get() = ShareController.getInstance(getApplication()).getShares() var visiblePassword = mutableStateOf>?>(null) private set @@ -183,6 +187,7 @@ class PasswordsViewModel(application: Application) : AndroidViewModel(applicatio _isRefreshing.emit(true) PasswordController.getInstance(getApplication()).syncPasswords() FolderController.getInstance(getApplication()).syncFolders() + ShareController.getInstance(getApplication()).syncShares() _isRefreshing.emit(false) } } else {