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
@@ -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;