209 lines
7.2 KiB
Kotlin
209 lines
7.2 KiB
Kotlin
package com.shaarit.presentation.add
|
|
|
|
import androidx.lifecycle.SavedStateHandle
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.shaarit.domain.model.ShaarliTag
|
|
import com.shaarit.domain.repository.AddLinkResult
|
|
import com.shaarit.domain.repository.LinkRepository
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
import java.net.URLDecoder
|
|
import javax.inject.Inject
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.launch
|
|
|
|
@HiltViewModel
|
|
class AddLinkViewModel
|
|
@Inject
|
|
constructor(private val linkRepository: LinkRepository, savedStateHandle: SavedStateHandle) :
|
|
ViewModel() {
|
|
|
|
// Pre-fill from usage arguments (e.g. from Share Intent via NavGraph)
|
|
private val initialUrl: String? = savedStateHandle["url"]
|
|
private val initialTitle: String? = savedStateHandle["title"]
|
|
|
|
private val _uiState = MutableStateFlow<AddLinkUiState>(AddLinkUiState.Idle)
|
|
val uiState = _uiState.asStateFlow()
|
|
|
|
var url = MutableStateFlow(decodeUrlParam(initialUrl) ?: "")
|
|
var title = MutableStateFlow(decodeUrlParam(initialTitle) ?: "")
|
|
var description = MutableStateFlow("")
|
|
var isPrivate = MutableStateFlow(false)
|
|
|
|
// New tag management
|
|
private val _selectedTags = MutableStateFlow<List<String>>(emptyList())
|
|
val selectedTags = _selectedTags.asStateFlow()
|
|
|
|
private val _newTagInput = MutableStateFlow("")
|
|
val newTagInput = _newTagInput.asStateFlow()
|
|
|
|
private val _availableTags = MutableStateFlow<List<ShaarliTag>>(emptyList())
|
|
val availableTags = _availableTags.asStateFlow()
|
|
|
|
private val _tagSuggestions = MutableStateFlow<List<ShaarliTag>>(emptyList())
|
|
val tagSuggestions = _tagSuggestions.asStateFlow()
|
|
|
|
// For conflict handling
|
|
private var conflictLinkId: Int? = null
|
|
|
|
init {
|
|
loadAvailableTags()
|
|
}
|
|
|
|
/** Decodes URL-encoded parameters, handling both + signs and %20 for spaces */
|
|
private fun decodeUrlParam(param: String?): String? {
|
|
if (param.isNullOrBlank()) return null
|
|
return try {
|
|
// First decode URL encoding, then replace + with spaces
|
|
// The + signs appear because URLEncoder uses + for spaces
|
|
URLDecoder.decode(param, "UTF-8").replace("+", " ").trim()
|
|
} catch (e: Exception) {
|
|
// If decoding fails, just replace + with spaces
|
|
param.replace("+", " ").trim()
|
|
}
|
|
}
|
|
|
|
private fun loadAvailableTags() {
|
|
viewModelScope.launch {
|
|
linkRepository
|
|
.getTags()
|
|
.fold(
|
|
onSuccess = { tags ->
|
|
_availableTags.value = tags.sortedByDescending { it.occurrences }
|
|
},
|
|
onFailure = {
|
|
// Silently fail - tags are optional
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onNewTagInputChanged(input: String) {
|
|
_newTagInput.value = input
|
|
updateTagSuggestions(input)
|
|
}
|
|
|
|
private fun updateTagSuggestions(query: String) {
|
|
if (query.isBlank()) {
|
|
_tagSuggestions.value = emptyList()
|
|
return
|
|
}
|
|
|
|
val queryLower = query.lowercase()
|
|
_tagSuggestions.value =
|
|
_availableTags
|
|
.value
|
|
.filter {
|
|
it.name.lowercase().contains(queryLower) &&
|
|
it.name !in _selectedTags.value
|
|
}
|
|
.take(10)
|
|
}
|
|
|
|
fun addTag(tag: String) {
|
|
val cleanTag = tag.trim().lowercase()
|
|
if (cleanTag.isNotBlank() && cleanTag !in _selectedTags.value) {
|
|
_selectedTags.value = _selectedTags.value + cleanTag
|
|
_newTagInput.value = ""
|
|
_tagSuggestions.value = emptyList()
|
|
}
|
|
}
|
|
|
|
fun addNewTag() {
|
|
addTag(_newTagInput.value)
|
|
}
|
|
|
|
fun removeTag(tag: String) {
|
|
_selectedTags.value = _selectedTags.value - tag
|
|
}
|
|
|
|
fun addLink() {
|
|
viewModelScope.launch {
|
|
_uiState.value = AddLinkUiState.Loading
|
|
|
|
// Basic validation
|
|
val currentUrl = url.value
|
|
if (currentUrl.isBlank()) {
|
|
_uiState.value = AddLinkUiState.Error("URL is required")
|
|
return@launch
|
|
}
|
|
|
|
val result =
|
|
linkRepository.addOrUpdateLink(
|
|
url = currentUrl,
|
|
title = title.value.ifBlank { null },
|
|
description = description.value.ifBlank { null },
|
|
tags = _selectedTags.value.ifEmpty { null },
|
|
isPrivate = isPrivate.value,
|
|
forceUpdate = false,
|
|
existingLinkId = null
|
|
)
|
|
|
|
when (result) {
|
|
is AddLinkResult.Success -> {
|
|
_uiState.value = AddLinkUiState.Success
|
|
}
|
|
is AddLinkResult.Conflict -> {
|
|
conflictLinkId = result.existingLinkId
|
|
_uiState.value =
|
|
AddLinkUiState.Conflict(
|
|
existingLinkId = result.existingLinkId,
|
|
existingTitle = result.existingTitle
|
|
)
|
|
}
|
|
is AddLinkResult.Error -> {
|
|
_uiState.value = AddLinkUiState.Error(result.message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun forceUpdateExistingLink() {
|
|
val linkId = conflictLinkId ?: return
|
|
|
|
viewModelScope.launch {
|
|
_uiState.value = AddLinkUiState.Loading
|
|
|
|
val result =
|
|
linkRepository.addOrUpdateLink(
|
|
url = url.value,
|
|
title = title.value.ifBlank { null },
|
|
description = description.value.ifBlank { null },
|
|
tags = _selectedTags.value.ifEmpty { null },
|
|
isPrivate = isPrivate.value,
|
|
forceUpdate = true,
|
|
existingLinkId = linkId
|
|
)
|
|
|
|
when (result) {
|
|
is AddLinkResult.Success -> {
|
|
_uiState.value = AddLinkUiState.Success
|
|
}
|
|
is AddLinkResult.Error -> {
|
|
_uiState.value = AddLinkUiState.Error(result.message)
|
|
}
|
|
else -> {
|
|
_uiState.value = AddLinkUiState.Error("Unexpected error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun dismissConflict() {
|
|
conflictLinkId = null
|
|
_uiState.value = AddLinkUiState.Idle
|
|
}
|
|
|
|
// Legacy compatibility for old comma-separated tags input
|
|
@Deprecated("Use selectedTags instead") var tags = MutableStateFlow("")
|
|
}
|
|
|
|
sealed class AddLinkUiState {
|
|
object Idle : AddLinkUiState()
|
|
object Loading : AddLinkUiState()
|
|
object Success : AddLinkUiState()
|
|
data class Error(val message: String) : AddLinkUiState()
|
|
data class Conflict(val existingLinkId: Int, val existingTitle: String?) : AddLinkUiState()
|
|
}
|