use string tag in domain layer

This commit is contained in:
2026-06-21 21:38:56 +03:00
parent 21a768edd5
commit b66fe60e48
25 changed files with 195 additions and 169 deletions
@@ -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}")
@@ -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,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" }
}
}
@@ -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
@@ -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)
)
)
}
@@ -42,7 +42,7 @@ class SyncClientSavedMovieRepository(
OperationInfo(
type = AggregateType.SAVED_MOVIE,
actionType = ACTION_SAVE,
payload = json.encodeToJsonElement(VideoDetails.serializer(), details)
payload = json.encodeToJsonElement(details)
)
)
}
@@ -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")
}
}
@@ -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")
}
@@ -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))
}
}
}
@@ -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))
}
}
}
@@ -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
}
}
@@ -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() {
@@ -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)
}
}
}
@@ -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"
}
@@ -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)
}
}
}
@@ -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()
}
)
@@ -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> {
@@ -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) }
)
}
}
@@ -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)
}
}
}
@@ -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()
}
},
@@ -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;
@@ -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>)
@@ -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(