From b66fe60e4895ed74b18da3c86a34e6dc6e4f89f1 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 21 Jun 2026 21:38:56 +0300 Subject: [PATCH] use string tag in domain layer --- .../data/BackendSavedMovieRepository.kt | 38 ++++++++-------- .../data/tags/BackendMovieTagRepository.kt | 33 ++++++++++---- .../data/tags/BackendUserTagRepository.kt | 16 ++----- .../presentation/routing/TagsRouting.kt | 17 +++---- .../shadowsparky/vbox/server/saved_movie.sq | 12 +++++ .../ru/shadowsparky/vbox/server/tags.sq | 13 +++--- ...ncClientServerRecentlyWatchedRepository.kt | 4 +- .../saved/SyncClientSavedMovieRepository.kt | 2 +- .../sync/strategy/MovieTagSyncStrategy.kt | 8 ++-- .../data/sync/strategy/UserTagSyncStrategy.kt | 12 ++--- .../data/tag/RemoteMovieTagRepository.kt | 18 ++++---- .../data/tag/RemoteUserTagRepository.kt | 8 ++-- .../shared/data/tag/SqlMovieTagRepository.kt | 35 ++++++++++----- .../shared/data/tag/SqlUserTagRepository.kt | 11 ++--- .../shared/data/tag/SyncMovieTagRepository.kt | 44 ++++++++----------- .../shared/data/tag/SyncUserTagRepository.kt | 18 ++++---- .../overview/OverviewComponent.kt | 6 +-- .../presentation/saved/SavedMovieScreen.kt | 4 +- .../saved/SavedMovieScreenComponent.kt | 8 ++-- .../settings/tag/LinkTagBottomSheet.kt | 8 ++-- .../presentation/settings/tag/TagComponent.kt | 8 ++-- .../presentation/settings/tag/TagScreen.kt | 12 ++--- .../ru/shadowsparky/vbox/domain/UserTag.sq | 10 +++-- .../shared/domain/tag/MovieTagRepository.kt | 10 ++--- .../shared/domain/tag/UserTagRepository.kt | 9 ++-- 25 files changed, 195 insertions(+), 169 deletions(-) diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendSavedMovieRepository.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendSavedMovieRepository.kt index a772d28..db35b0c 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendSavedMovieRepository.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendSavedMovieRepository.kt @@ -1,11 +1,13 @@ package ru.shadowsparky.vbox.backend.data +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import ru.shadowsparky.backend.data.Logger import ru.shadowsparky.vbox.backend.AppDatabase -import ru.shadowsparky.vbox.server.Saved_movie import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.domain.RemoteEvent import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler @@ -23,26 +25,22 @@ class BackendSavedMovieRepository( ) : SavedMovieRepository { override fun getAll(): Flow> { - val request = db.saved_movieQueries.selectByUserId(userId) - return flow { - emit(request.executeAsList().parse().reversed()) - } - } - - private suspend fun List.parse(): List = - withContext(dispatcherProvider.io) { - mapNotNull { - val movie = db.movieQueries.selectMovie(it.movie_id) - .executeAsOneOrNull() ?: return@mapNotNull null - VideoDetails( - movie.movie_id, - movie.poster_url, - movie.description, - movie.title, - (movie.flags and SERIAL_FLAG) != 0 - ) + return db.saved_movieQueries + .selectDetailsByUserId(userId) + .asFlow() + .mapToList(dispatcherProvider.io) + .map { list -> + list.map { movie -> + VideoDetails( + id = movie.movie_id, + poster = movie.poster_url, + desc = movie.description, + title = movie.title, + isSerial = (movie.flags and SERIAL_FLAG) != 0 + ) + } } - } + } override suspend fun save(details: VideoDetails) { logger.debug(TAG, "save(${details.id}) ${details.title}") diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendMovieTagRepository.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendMovieTagRepository.kt index b4028a1..919e9a9 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendMovieTagRepository.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendMovieTagRepository.kt @@ -3,10 +3,14 @@ package ru.shadowsparky.vbox.backend.data.tags import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext import org.koin.core.annotation.Factory import org.koin.core.annotation.Named import ru.shadowsparky.domain.DispatcherProvider +import ru.shadowsparky.http.domain.BadRequestException import ru.shadowsparky.vbox.backend.AppDatabase import ru.shadowsparky.vbox.shared.domain.EventType import ru.shadowsparky.vbox.shared.domain.RemoteEvent @@ -46,19 +50,30 @@ class BackendMovieTagRepository( list.map { row -> TagInfo(id = row.id, tag = row.tag) } } - override fun queryMovies(tagId: Long): Flow> = dbQueries - .getMoviesByTagId(tagId, userId) - .asFlow() - .mapToList(dispatcherProvider.io) - .map { list -> list.toSet() } + override fun queryMovies(tag: String): Flow> { + return flow { + val id = findTagId(tag) + emitAll( + dbQueries.getMoviesByTagId(id, userId) + .asFlow() + .mapToList(dispatcherProvider.io) + .map { list -> list.toSet() } + ) + } + } - override suspend fun link(movieId: Long, tagId: Long) { - dbQueries.linkMovieTag(movieId, tagId, userId).await() + override suspend fun link(movieId: Long, tag: String) { + dbQueries.linkMovieTag(movieId, findTagId(tag), userId).await() movieEventHandler.notify(RemoteEvent.OnMovieTag(userId, movieId)) } - override suspend fun unlink(movieId: Long, tagId: Long) { - dbQueries.unlinkMovieTag(movieId, tagId, userId).await() + override suspend fun unlink(movieId: Long, tag: String) { + dbQueries.unlinkMovieTag(movieId, findTagId(tag), userId).await() movieEventHandler.notify(RemoteEvent.OnMovieTag(userId, movieId)) } + + private suspend fun findTagId(tag: String): Long = withContext(dispatcherProvider.io) { + db.tagsQueries.findTagId(tag, userId).executeAsOneOrNull() + ?: throw BadRequestException("tag $tag not found for user with id $userId") + } } diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendUserTagRepository.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendUserTagRepository.kt index 84ddb59..aac4bf9 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendUserTagRepository.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/tags/BackendUserTagRepository.kt @@ -4,7 +4,6 @@ import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext import org.koin.core.annotation.Factory import org.koin.core.annotation.Named import ru.shadowsparky.vbox.backend.AppDatabase @@ -53,20 +52,13 @@ class BackendUserTagRepository( userTagEventHandler.notify(RemoteEvent.OnUserTag(userId)) } - override suspend fun edit(id: Long, newTag: String) { - checkOwner(id) - queries.updateTagText(newTag, id).await() + override suspend fun edit(oldTag: String, newTag: String) { + queries.updateTagText(newTag, oldTag, userId).await() userTagEventHandler.notify(RemoteEvent.OnUserTag(userId)) } - override suspend fun delete(id: Long) { - checkOwner(id) - queries.deleteTagById(id).await() + override suspend fun delete(tag: String) { + queries.deleteTagById(tag, userId).await() userTagEventHandler.notify(RemoteEvent.OnUserTag(userId)) } - - private suspend fun checkOwner(id: Long) = withContext(dispatcherProvider.io) { - val ownerId = queries.getUserIdByTagId(id).executeAsOneOrNull() - check(ownerId == userId) { "tag owner mismatch. owner: $ownerId, current user: $userId" } - } } diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/TagsRouting.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/TagsRouting.kt index e7725c2..d37dedd 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/TagsRouting.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/TagsRouting.kt @@ -7,6 +7,7 @@ import io.ktor.server.routing.Route import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post +import io.ktor.server.util.getOrFail import kotlinx.coroutines.flow.first import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory @@ -41,12 +42,12 @@ private fun Route.setupUserTag(userTagFactory: UserTagRepositoryFactory) { post(UserTagRepository.EDIT) { val userTag = userTagFactory.create(call.obtainUserId()) val edit = call.receive() - userTag.edit(edit.id, edit.newTag) + userTag.edit(edit.oldTag, edit.newTag) call.respond(HttpStatusCode.OK) } delete(UserTagRepository.DELETE) { val userTag = userTagFactory.create(call.obtainUserId()) - userTag.delete(call.receive().id) + userTag.delete(call.receive().tag) call.respond(HttpStatusCode.OK) } } @@ -54,26 +55,26 @@ private fun Route.setupUserTag(userTagFactory: UserTagRepositoryFactory) { private fun Route.setupMovieTag(movieTagFactory: MovieTagRepositoryFactory) { get(MovieTagRepository.QUERY) { val movieTag = movieTagFactory.create(call.obtainUserId()) - val movieId = call.parameters[MovieTagRepository.MOVIE_ID]?.toLong() - val tags = movieTag.query(checkNotNull(movieId) { "movieId required" }).first() + val movieId = call.parameters.getOrFail(MovieTagRepository.MOVIE_ID) + val tags = movieTag.query(movieId).first() call.respond(TagResponse(tags)) } get(MovieTagRepository.QUERY_MOVIES) { val movieTag = movieTagFactory.create(call.obtainUserId()) - val tagId = call.parameters[MovieTagRepository.TAG_ID]?.toLong() - val ids = movieTag.queryMovies(checkNotNull(tagId) { "tagId required" }).first() + val tagName = call.parameters.getOrFail(MovieTagRepository.TAG_NAME) + val ids = movieTag.queryMovies(tagName).first() call.respond(LinkedMovies(ids)) } post(MovieTagRepository.LINK) { val movieTag = movieTagFactory.create(call.obtainUserId()) val link = call.receive() - movieTag.link(link.movieId, link.tagId) + movieTag.link(link.movieId, link.tag) call.respond(HttpStatusCode.OK) } delete(MovieTagRepository.UNLINK) { val movieTag = movieTagFactory.create(call.obtainUserId()) val link = call.receive() - movieTag.unlink(link.movieId, link.tagId) + movieTag.unlink(link.movieId, link.tag) call.respond(HttpStatusCode.OK) } } diff --git a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/saved_movie.sq b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/saved_movie.sq index 3d37c77..747f8ea 100644 --- a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/saved_movie.sq +++ b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/saved_movie.sq @@ -11,3 +11,15 @@ addSavedMovie: removeSavedMovie: DELETE FROM "saved_movie" WHERE user_id = ? AND movie_id = ?; + +selectDetailsByUserId: +SELECT + m.movie_id, + m.poster_url, + m.description, + m.title, + m.flags +FROM saved_movie AS sm +JOIN movie AS m ON sm.movie_id = m.movie_id +WHERE sm.user_id = ? +ORDER BY sm.saved_movie_id DESC; diff --git a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/tags.sq b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/tags.sq index ced3b19..bff2ea4 100644 --- a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/tags.sq +++ b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/tags.sq @@ -3,11 +3,6 @@ SELECT id, user_id, tag FROM user_tag WHERE user_id = ?; -getUserIdByTagId: -SELECT user_id -FROM user_tag -WHERE id = ?; - insertUserTag: INSERT INTO user_tag (user_id, tag) VALUES (?, ?); @@ -15,11 +10,15 @@ VALUES (?, ?); updateTagText: UPDATE user_tag SET tag = ? -WHERE id = ?; +WHERE tag = ? AND user_id = ?; deleteTagById: DELETE FROM user_tag -WHERE id = ?; +WHERE tag = ? AND user_id = ?; + +findTagId: +SELECT id FROM user_tag +WHERE tag = ? AND user_id = ?; getTagsForMovie: SELECT ut.id, ut.tag diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt index 228bcc1..4a33de5 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt @@ -71,7 +71,7 @@ class SyncClientServerRecentlyWatchedRepository( OperationInfo( type = AggregateType.RECENTLY_WATCHED, actionType = ACTION_WRITE, - payload = json.encodeToJsonElement(RecentlyWatchedInfo.serializer(), info) + payload = json.encodeToJsonElement(info) ) ) } @@ -88,7 +88,7 @@ class SyncClientServerRecentlyWatchedRepository( OperationInfo( type = AggregateType.RECENTLY_WATCHED, actionType = ACTION_REMOVE, - payload = json.encodeToJsonElement(RecentlyWatchedInfo.serializer(), info) + payload = json.encodeToJsonElement(info) ) ) } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt index fd85ce0..8f9b1a5 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt @@ -42,7 +42,7 @@ class SyncClientSavedMovieRepository( OperationInfo( type = AggregateType.SAVED_MOVIE, actionType = ACTION_SAVE, - payload = json.encodeToJsonElement(VideoDetails.serializer(), details) + payload = json.encodeToJsonElement(details) ) ) } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/MovieTagSyncStrategy.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/MovieTagSyncStrategy.kt index 18ffeed..a53bbef 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/MovieTagSyncStrategy.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/MovieTagSyncStrategy.kt @@ -22,11 +22,11 @@ class MovieTagSyncStrategy( val payloadObj = payload.jsonObject val movieId = payloadObj[SyncMovieTagRepository.KEY_MOVIE_ID]?.jsonPrimitive?.long ?: error("Missing ${SyncMovieTagRepository.KEY_MOVIE_ID}") - val tagId = payloadObj[SyncMovieTagRepository.KEY_TAG_ID]?.jsonPrimitive?.long - ?: error("Missing ${SyncMovieTagRepository.KEY_TAG_ID}") + val tagName = payloadObj[SyncMovieTagRepository.KEY_TAG_NAME]?.jsonPrimitive?.content + ?: error("Missing ${SyncMovieTagRepository.KEY_TAG_NAME}") when (actionType) { - SyncMovieTagRepository.ACTION_LINK -> remote.link(movieId, tagId) - SyncMovieTagRepository.ACTION_UNLINK -> remote.unlink(movieId, tagId) + SyncMovieTagRepository.ACTION_LINK -> remote.link(movieId, tagName) + SyncMovieTagRepository.ACTION_UNLINK -> remote.unlink(movieId, tagName) else -> error("Unknown action type: $actionType") } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/UserTagSyncStrategy.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/UserTagSyncStrategy.kt index c2f1300..f6ad073 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/UserTagSyncStrategy.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/UserTagSyncStrategy.kt @@ -27,16 +27,16 @@ class UserTagSyncStrategy( remote.add(tag) } SyncUserTagRepository.ACTION_EDIT -> { - val id = payloadObj[SyncUserTagRepository.KEY_ID]?.jsonPrimitive?.long - ?: error("Missing ${SyncUserTagRepository.KEY_ID}") + val oldTag = payloadObj[SyncUserTagRepository.KEY_OLD_TAG]?.jsonPrimitive?.content + ?: error("Missing ${SyncUserTagRepository.KEY_OLD_TAG}") val newTag = payloadObj[SyncUserTagRepository.KEY_NEW_TAG]?.jsonPrimitive?.content ?: error("Missing ${SyncUserTagRepository.KEY_NEW_TAG}") - remote.edit(id, newTag) + remote.edit(oldTag, newTag) } SyncUserTagRepository.ACTION_DELETE -> { - val id = payloadObj[SyncUserTagRepository.KEY_ID]?.jsonPrimitive?.long - ?: error("Missing ${SyncUserTagRepository.KEY_ID}") - remote.delete(id) + val oldTag = payloadObj[SyncUserTagRepository.KEY_OLD_TAG]?.jsonPrimitive?.content + ?: error("Missing ${SyncUserTagRepository.KEY_OLD_TAG}") + remote.delete(oldTag) } else -> error("Unknown action type: $actionType") } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteMovieTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteMovieTagRepository.kt index 8acd810..01d089e 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteMovieTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteMovieTagRepository.kt @@ -52,34 +52,34 @@ class RemoteMovieTagRepository( ).tags } - override fun queryMovies(tagId: Long): Flow> { + override fun queryMovies(tag: String): Flow> { return movieTagListener.event.filterIsInstance() - .map { queryMoviesSingle(tagId) } - .onStart { emit(queryMoviesSingle(tagId)) } + .map { queryMoviesSingle(tag) } + .onStart { emit(queryMoviesSingle(tag)) } .flowOn(dispatcherProvider.io) } - private suspend fun queryMoviesSingle(tagId: Long): Set { + private suspend fun queryMoviesSingle(tag: String): Set { return httpClient.get( MovieTagRepository.QUERY_MOVIES, - hashMapOf(MovieTagRepository.TAG_ID to tagId.toString()), + hashMapOf(MovieTagRepository.TAG_NAME to tag), serverConfigurationProvider.getServerConfiguration() ).ids } - override suspend fun link(movieId: Long, tagId: Long) { + override suspend fun link(movieId: Long, tag: String) { httpClient.post { serverConfigurationProvider.getServerConfiguration() .apply(this, MovieTagRepository.LINK) - setJsonBody(LinkRequest(movieId, tagId)) + setJsonBody(LinkRequest(movieId, tag)) } } - override suspend fun unlink(movieId: Long, tagId: Long) { + override suspend fun unlink(movieId: Long, tag: String) { httpClient.delete { serverConfigurationProvider.getServerConfiguration() .apply(this, MovieTagRepository.UNLINK) - setJsonBody(LinkRequest(movieId, tagId)) + setJsonBody(LinkRequest(movieId, tag)) } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteUserTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteUserTagRepository.kt index ac4eadb..8b41b0c 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteUserTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/RemoteUserTagRepository.kt @@ -57,19 +57,19 @@ class RemoteUserTagRepository( } } - override suspend fun edit(id: Long, newTag: String) { + override suspend fun edit(oldTag: String, newTag: String) { httpClient.post { serverConfigurationProvider.getServerConfiguration() .apply(this, UserTagRepository.EDIT) - setJsonBody(EditRequest(id, newTag)) + setJsonBody(EditRequest(oldTag, newTag)) } } - override suspend fun delete(id: Long) { + override suspend fun delete(tag: String) { httpClient.delete { serverConfigurationProvider.getServerConfiguration() .apply(this, UserTagRepository.DELETE) - setJsonBody(DeleteRequest(id)) + setJsonBody(DeleteRequest(tag)) } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlMovieTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlMovieTagRepository.kt index 5bd27dd..2596df3 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlMovieTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlMovieTagRepository.kt @@ -6,10 +6,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext import org.koin.core.annotation.Factory import org.koin.core.annotation.Named import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.vbox.domain.MovieTagQueries +import ru.shadowsparky.vbox.domain.UserTagQueries import ru.shadowsparky.vbox.shared.data.AppDatabaseProvider import ru.shadowsparky.vbox.shared.di.DbType import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository @@ -38,26 +40,29 @@ class SqlMovieTagRepository( ) } - override fun queryMovies(tagId: Long): Flow> = flow { - emitAll( - getQueries().queryMovies(tagId) - .asFlow() - .mapToList(dispatcherProvider.io) - .map { it.toSet() } - ) + override fun queryMovies(tag: String): Flow> = flow { + val id = queryId(tag) + if (id != null) { + emitAll( + getQueries().queryMovies(id) + .asFlow() + .mapToList(dispatcherProvider.io) + .map { it.toSet() } + ) + } } - override suspend fun link(movieId: Long, tagId: Long) { + override suspend fun link(movieId: Long, tag: String) { getQueries().link( movieId = movieId, - tagId = tagId + tagId = queryId(tag) ?: return ) } - override suspend fun unlink(movieId: Long, tagId: Long) { + override suspend fun unlink(movieId: Long, tag: String) { getQueries().unlink( movieId = movieId, - tagId = tagId + tagId = queryId(tag) ?: return ) } @@ -65,7 +70,15 @@ class SqlMovieTagRepository( getQueries().clear() } + private suspend fun queryId(tag: String): Long? = withContext(dispatcherProvider.io) { + getTagsQueries().find(tag).executeAsOneOrNull()?.id + } + private suspend fun getQueries(): MovieTagQueries { return dbProvider.get().movieTagQueries } + + private suspend fun getTagsQueries(): UserTagQueries { + return dbProvider.get().userTagQueries + } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlUserTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlUserTagRepository.kt index b013c86..e722094 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlUserTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SqlUserTagRepository.kt @@ -42,15 +42,12 @@ class SqlUserTagRepository( runCatching { getQueries().insert(tag) } } - override suspend fun edit(id: Long, newTag: String) { - getQueries().update( - id = id, - newTag = newTag - ) + override suspend fun edit(oldTag: String, newTag: String) { + getQueries().update(newTag, oldTag) } - override suspend fun delete(id: Long) { - runCatching { getQueries().delete(id) } + override suspend fun delete(tag: String) { + runCatching { getQueries().delete(tag) } } override suspend fun clear() { diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt index 50e7f5e..e9c7f7b 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt @@ -48,69 +48,63 @@ class SyncMovieTagRepository( queryRemote = { val allTags = userTagRemote.userTags.first() allTags.flatMap { tag -> - val tagId = tag.id ?: return@flatMap emptyList() - remote.queryMovies(tagId).first().map { movieId -> - MovieTagLink(movieId = movieId, tagId = tagId) - } + remote.queryMovies(tag.tag).first().map { movieId -> MovieTagLink(movieId, tag.tag) } } }, queryLocal = { val allTags = userTagLocal.userTags.first() allTags.flatMap { tag -> - val tagId = tag.id ?: return@flatMap emptyList() - local.queryMovies(tagId).first().map { movieId -> - MovieTagLink(movieId = movieId, tagId = tagId) - } + local.queryMovies(tag.tag).first().map { movieId -> MovieTagLink(movieId, tag.tag) } } }, writeRemote = { list -> - list.forEach { link -> remote.link(movieId = link.movieId, tagId = link.tagId) } + list.forEach { link -> remote.link(link.movieId, link.tag) } }, writeLocal = { list -> - list.forEach { link -> local.link(movieId = link.movieId, tagId = link.tagId) } + list.forEach { link -> local.link(link.movieId, link.tag) } }, - selectKey = { "${it.movieId}|${it.tagId}" } + selectKey = { "${it.movieId}|${it.tag}" } ) } @Serializable data class MovieTagLink( val movieId: Long, - val tagId: Long + val tag: String ) - override fun queryMovies(tagId: Long): Flow> { - return tokenStorage.queryFlow(remote.queryMovies(tagId), local.queryMovies(tagId)) + override fun queryMovies(tag: String): Flow> { + return tokenStorage.queryFlow(remote.queryMovies(tag), local.queryMovies(tag)) } - override suspend fun link(movieId: Long, tagId: Long) { - local.link(movieId, tagId) + override suspend fun link(movieId: Long, tag: String) { + local.link(movieId, tag) if (tokenStorage.hasAccount()) { try { - remote.link(movieId, tagId) + remote.link(movieId, tag) } catch (_: Exception) { pendingRepo.insert( OperationInfo( type = AggregateType.MOVIE_TAG, actionType = ACTION_LINK, - payload = buildPayload(movieId, tagId) + payload = buildPayload(movieId, tag) ) ) } } } - override suspend fun unlink(movieId: Long, tagId: Long) { - local.unlink(movieId, tagId) + override suspend fun unlink(movieId: Long, tag: String) { + local.unlink(movieId, tag) if (tokenStorage.hasAccount()) { try { - remote.unlink(movieId, tagId) + remote.unlink(movieId, tag) } catch (_: Exception) { pendingRepo.insert( OperationInfo( type = AggregateType.MOVIE_TAG, actionType = ACTION_UNLINK, - payload = buildPayload(movieId, tagId) + payload = buildPayload(movieId, tag) ) ) } @@ -127,11 +121,11 @@ class SyncMovieTagRepository( const val ACTION_UNLINK = "UNLINK" const val KEY_MOVIE_ID = "movieId" - const val KEY_TAG_ID = "tagId" + const val KEY_TAG_NAME = "tagName" - private fun buildPayload(movieId: Long, tagId: Long) = buildJsonObject { + private fun buildPayload(movieId: Long, tagName: String) = buildJsonObject { put(KEY_MOVIE_ID, movieId) - put(KEY_TAG_ID, tagId) + put(KEY_TAG_NAME, tagName) } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt index 15daef5..007f9b4 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt @@ -72,14 +72,14 @@ class SyncUserTagRepository( } } - override suspend fun edit(id: Long, newTag: String) { - local.edit(id, newTag) + override suspend fun edit(oldTag: String, newTag: String) { + local.edit(oldTag, newTag) if (tokenStorage.hasAccount()) { try { - remote.edit(id, newTag) + remote.edit(oldTag, newTag) } catch (_: Exception) { val payload = buildJsonObject { - put(KEY_ID, id) + put(KEY_OLD_TAG, oldTag) put(KEY_NEW_TAG, newTag) } pendingRepo.insert( @@ -93,13 +93,13 @@ class SyncUserTagRepository( } } - override suspend fun delete(id: Long) { - local.delete(id) + override suspend fun delete(tag: String) { + local.delete(tag) if (tokenStorage.hasAccount()) { try { - remote.delete(id) + remote.delete(tag) } catch (_: Exception) { - val payload = buildJsonObject { put(KEY_ID, id) } + val payload = buildJsonObject { put(KEY_OLD_TAG, tag) } pendingRepo.insert( OperationInfo( type = AggregateType.USER_TAG, @@ -121,7 +121,7 @@ class SyncUserTagRepository( const val ACTION_EDIT = "EDIT" const val ACTION_DELETE = "DELETE" - const val KEY_ID = "id" + const val KEY_OLD_TAG = "old_tag" const val KEY_TAG = "tag" const val KEY_NEW_TAG = "newTag" } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewComponent.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewComponent.kt index 5f52e62..14731cc 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewComponent.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewComponent.kt @@ -72,12 +72,12 @@ class OverviewComponent( doOnStart { init() } } - fun updateTag(tagId: Long, state: Boolean) { + fun updateTag(tag: String, state: Boolean) { exec { if (state) { - movieTagRepository.link(id, tagId) + movieTagRepository.link(id, tag) } else { - movieTagRepository.unlink(id, tagId) + movieTagRepository.unlink(id, tag) } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt index 72a2986..946b96f 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt @@ -81,7 +81,7 @@ fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingVal private fun FilterBottomSheet( allTags: List?, onDismiss: () -> Unit, - onFilterBy: (Long?) -> Unit + onFilterBy: (String?) -> Unit ) { allTags ?: return ModalBottomSheet(onDismissRequest = onDismiss) { @@ -113,7 +113,7 @@ private fun FilterBottomSheet( TextPreference( text = tag.tag, modifier = Modifier.clickable { - onFilterBy(tag.id) + onFilterBy(tag.tag) onDismiss() } ) diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreenComponent.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreenComponent.kt index 6b4485d..366324d 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreenComponent.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreenComponent.kt @@ -29,11 +29,11 @@ class SavedMovieScreenComponent( ) : ComponentContext by context { val userTags = userTagRepository.userTags - private val filterIdFlow = MutableStateFlow(null) + private val filterTagFlow = MutableStateFlow(null) private val allMovies = repo.getAll().map { it.map() } - private val filterMovieIds = filterIdFlow.flatMapLatest { + private val filterMovieIds = filterTagFlow.flatMapLatest { if (it == null) { flowOf(null) } else { @@ -49,8 +49,8 @@ class SavedMovieScreenComponent( } } - fun filterByTag(tagId: Long?) { - filterIdFlow.tryEmit(tagId) + fun filterByTag(tagId: String?) { + filterTagFlow.tryEmit(tagId) } private fun List.map(): List { diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/LinkTagBottomSheet.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/LinkTagBottomSheet.kt index 2265807..8c30bf8 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/LinkTagBottomSheet.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/LinkTagBottomSheet.kt @@ -33,7 +33,7 @@ fun LinkTagBottomSheet( linkedTags: List?, onDismiss: () -> Unit, requestTagsScreen: () -> Unit, - updateTag: (Long, Boolean) -> Unit + updateTag: (String, Boolean) -> Unit ) { ModalBottomSheet(onDismissRequest = onDismiss) { Column( @@ -70,7 +70,7 @@ fun LinkTagBottomSheet( private fun ShowTags( allTags: List, linkedTags: List, - updateTag: (Long, Boolean) -> Unit + updateTag: (String, Boolean) -> Unit ) { val linkedTagIds = remember(linkedTags) { linkedTags.mapNotNull { it.id }.toSet() @@ -90,7 +90,7 @@ private fun ShowTags( Row( modifier = Modifier .fillMaxWidth() - .clickable { updateTag(tagId, !isChecked) } + .clickable { updateTag(tag.tag, !isChecked) } .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -102,7 +102,7 @@ private fun ShowTags( ) Checkbox( checked = isChecked, - onCheckedChange = { checked -> updateTag(tagId, checked) } + onCheckedChange = { checked -> updateTag(tag.tag, checked) } ) } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagComponent.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagComponent.kt index 62a15df..2446730 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagComponent.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagComponent.kt @@ -27,15 +27,15 @@ class TagComponent( } } - fun edit(id: Long, newTag: String) { + fun edit(oldTag: String, newTag: String) { scope.launch { - tagRepository.edit(id, newTag) + tagRepository.edit(oldTag, newTag) } } - fun delete(id: Long) { + fun delete(tag: String) { scope.launch { - tagRepository.delete(id) + tagRepository.delete(tag) } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagScreen.kt index b6fddf2..f66e858 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/settings/tag/TagScreen.kt @@ -89,8 +89,8 @@ fun TagScreen(component: TagComponent) { EditTagBottomSheet( activeTag, onDismiss = { activeTag = null }, - onEdit = { id, tag -> component.edit(id, tag.trim()) }, - onDelete = { id -> component.delete(id) }, + onEdit = { oldTag, tag -> component.edit(oldTag, tag.trim()) }, + onDelete = { oldTag -> component.delete(oldTag) }, canSave = canSave ) } else if (isAddSheetVisible) { @@ -155,8 +155,8 @@ private fun AddTagBottomSheet( private fun EditTagBottomSheet( tag: TagInfo? = null, onDismiss: () -> Unit, - onEdit: (Long, String) -> Unit, - onDelete: (Long) -> Unit, + onEdit: (String, String) -> Unit, + onDelete: (String) -> Unit, canSave: (String) -> Boolean ) { tag ?: return @@ -184,7 +184,7 @@ private fun EditTagBottomSheet( Button( onClick = { if (tagId != null && newTag.isNotEmpty()) { - onEdit(tagId, newTag) + onEdit(tag.tag, newTag) onDismiss() } }, @@ -194,7 +194,7 @@ private fun EditTagBottomSheet( OutlinedButton( onClick = { if (tagId != null) { - onDelete(tagId) + onDelete(tag.tag) onDismiss() } }, diff --git a/apps/vbox/client/shared/src/commonMain/sqldelight/ru/shadowsparky/vbox/domain/UserTag.sq b/apps/vbox/client/shared/src/commonMain/sqldelight/ru/shadowsparky/vbox/domain/UserTag.sq index 0723d69..576d8bf 100644 --- a/apps/vbox/client/shared/src/commonMain/sqldelight/ru/shadowsparky/vbox/domain/UserTag.sq +++ b/apps/vbox/client/shared/src/commonMain/sqldelight/ru/shadowsparky/vbox/domain/UserTag.sq @@ -5,8 +5,12 @@ CREATE TABLE UserTag ( selectAll: SELECT id, tag +FROM UserTag; + +find: +SELECT id, tag FROM UserTag -ORDER BY id DESC; +WHERE tag = ?; insert: INSERT OR IGNORE INTO UserTag(tag) @@ -15,11 +19,11 @@ VALUES (?); update: UPDATE UserTag SET tag = :newTag -WHERE id = :id; +WHERE tag = ?; delete: DELETE FROM UserTag -WHERE id = ?; +WHERE tag = ?; clear: DELETE FROM UserTag; diff --git a/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/MovieTagRepository.kt b/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/MovieTagRepository.kt index a73a043..ae7359a 100644 --- a/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/MovieTagRepository.kt +++ b/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/MovieTagRepository.kt @@ -6,14 +6,14 @@ import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX interface MovieTagRepository { fun query(movieId: Long): Flow> - fun queryMovies(tagId: Long): Flow> - suspend fun link(movieId: Long, tagId: Long) - suspend fun unlink(movieId: Long, tagId: Long) + fun queryMovies(tag: String): Flow> + suspend fun link(movieId: Long, tag: String) + suspend fun unlink(movieId: Long, tag: String) suspend fun clear() {} companion object { - const val TAG_ID = "tagId" + const val TAG_NAME = "tagName" const val MOVIE_ID = "movieId" const val PREFIX = "$DYNAMIC_PREFIX/movieTag" @@ -25,7 +25,7 @@ interface MovieTagRepository { } @Serializable -data class LinkRequest(val movieId: Long, val tagId: Long) +data class LinkRequest(val movieId: Long, val tag: String) @Serializable data class LinkedMovies(val ids: Set) diff --git a/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/UserTagRepository.kt b/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/UserTagRepository.kt index a8a7446..727c0bb 100644 --- a/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/UserTagRepository.kt +++ b/apps/vbox/common/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/tag/UserTagRepository.kt @@ -8,8 +8,8 @@ interface UserTagRepository { val userTags: Flow> suspend fun add(tag: String) - suspend fun edit(id: Long, newTag: String) - suspend fun delete(id: Long) + suspend fun edit(oldTag: String, newTag: String) + suspend fun delete(tag: String) suspend fun clear() {} @@ -18,6 +18,7 @@ interface UserTagRepository { const val LIST = "$PREFIX/list" const val ADD = "$PREFIX/add" + const val EDIT = "$PREFIX/edit" const val DELETE = "$PREFIX/delete" } @@ -30,10 +31,10 @@ data class TagResponse(val tags: List) data class AddRequest(val tag: String) @Serializable -data class EditRequest(val id: Long, val newTag: String) +data class EditRequest(val oldTag: String, val newTag: String) @Serializable -data class DeleteRequest(val id: Long) +data class DeleteRequest(val tag: String) @Serializable data class TagInfo(