use string tag in domain layer
This commit is contained in:
+18
-20
@@ -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<List<VideoDetails>> {
|
||||
val request = db.saved_movieQueries.selectByUserId(userId)
|
||||
return flow {
|
||||
emit(request.executeAsList().parse().reversed())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun List<Saved_movie>.parse(): List<VideoDetails> =
|
||||
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}")
|
||||
|
||||
+24
-9
@@ -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<Set<Long>> = dbQueries
|
||||
.getMoviesByTagId(tagId, userId)
|
||||
.asFlow()
|
||||
.mapToList(dispatcherProvider.io)
|
||||
.map { list -> list.toSet() }
|
||||
override fun queryMovies(tag: String): Flow<Set<Long>> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-12
@@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -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<EditRequest>()
|
||||
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<DeleteRequest>().id)
|
||||
userTag.delete(call.receive<DeleteRequest>().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<Long>(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<LinkRequest>()
|
||||
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<LinkRequest>()
|
||||
movieTag.unlink(link.movieId, link.tagId)
|
||||
movieTag.unlink(link.movieId, link.tag)
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class SyncClientSavedMovieRepository(
|
||||
OperationInfo(
|
||||
type = AggregateType.SAVED_MOVIE,
|
||||
actionType = ACTION_SAVE,
|
||||
payload = json.encodeToJsonElement(VideoDetails.serializer(), details)
|
||||
payload = json.encodeToJsonElement(details)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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")
|
||||
}
|
||||
|
||||
+9
-9
@@ -52,34 +52,34 @@ class RemoteMovieTagRepository(
|
||||
).tags
|
||||
}
|
||||
|
||||
override fun queryMovies(tagId: Long): Flow<Set<Long>> {
|
||||
override fun queryMovies(tag: String): Flow<Set<Long>> {
|
||||
return movieTagListener.event.filterIsInstance<RemoteEvent.OnMovieTag>()
|
||||
.map { queryMoviesSingle(tagId) }
|
||||
.onStart { emit(queryMoviesSingle(tagId)) }
|
||||
.map { queryMoviesSingle(tag) }
|
||||
.onStart { emit(queryMoviesSingle(tag)) }
|
||||
.flowOn(dispatcherProvider.io)
|
||||
}
|
||||
|
||||
private suspend fun queryMoviesSingle(tagId: Long): Set<Long> {
|
||||
private suspend fun queryMoviesSingle(tag: String): Set<Long> {
|
||||
return httpClient.get<LinkedMovies>(
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-11
@@ -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<Set<Long>> = flow {
|
||||
emitAll(
|
||||
getQueries().queryMovies(tagId)
|
||||
.asFlow()
|
||||
.mapToList(dispatcherProvider.io)
|
||||
.map { it.toSet() }
|
||||
)
|
||||
override fun queryMovies(tag: String): Flow<Set<Long>> = 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
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -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() {
|
||||
|
||||
+19
-25
@@ -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<Set<Long>> {
|
||||
return tokenStorage.queryFlow(remote.queryMovies(tagId), local.queryMovies(tagId))
|
||||
override fun queryMovies(tag: String): Flow<Set<Long>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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"
|
||||
}
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingVal
|
||||
private fun FilterBottomSheet(
|
||||
allTags: List<TagInfo>?,
|
||||
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()
|
||||
}
|
||||
)
|
||||
|
||||
+4
-4
@@ -29,11 +29,11 @@ class SavedMovieScreenComponent(
|
||||
) : ComponentContext by context {
|
||||
val userTags = userTagRepository.userTags
|
||||
|
||||
private val filterIdFlow = MutableStateFlow<Long?>(null)
|
||||
private val filterTagFlow = MutableStateFlow<String?>(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<VideoDetails>.map(): List<VideoItem> {
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@ fun LinkTagBottomSheet(
|
||||
linkedTags: List<TagInfo>?,
|
||||
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<TagInfo>,
|
||||
linkedTags: List<TagInfo>,
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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()
|
||||
}
|
||||
},
|
||||
|
||||
+7
-3
@@ -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;
|
||||
|
||||
+5
-5
@@ -6,14 +6,14 @@ import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
||||
|
||||
interface MovieTagRepository {
|
||||
fun query(movieId: Long): Flow<List<TagInfo>>
|
||||
fun queryMovies(tagId: Long): Flow<Set<Long>>
|
||||
suspend fun link(movieId: Long, tagId: Long)
|
||||
suspend fun unlink(movieId: Long, tagId: Long)
|
||||
fun queryMovies(tag: String): Flow<Set<Long>>
|
||||
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<Long>)
|
||||
|
||||
+5
-4
@@ -8,8 +8,8 @@ interface UserTagRepository {
|
||||
val userTags: Flow<List<TagInfo>>
|
||||
|
||||
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<TagInfo>)
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user