2026-01-11 19:47:49 -05:00

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()
}