package com.vgmlr.wedge
import android.content.Context
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class MainViewModel(
val dao: NoteDao,
val prefs: PreferenceManager,
context: Context
) : ViewModel() {
private val appContext = context.applicationContext
private val syncPrefs = appContext.getSharedPreferences("sync_prefs", Context.MODE_PRIVATE)
private val _syncStatus = MutableStateFlow("Disconnected")
val syncStatus = _syncStatus.asStateFlow()
private val _isLinkUp = MutableStateFlow(false)
val isLinkUp = _isLinkUp.asStateFlow()
private val _peerLastModified = MutableStateFlow(0L)
private val _autoMerge = MutableStateFlow(syncPrefs.getBoolean("auto_merge", false))
val autoMerge = _autoMerge.asStateFlow()
private var idleJob: Job? = null
// bump editor on sync
private val _syncEvent = MutableStateFlow(0L)
val syncEvent = _syncEvent.asStateFlow()
private var currentPassKey = ""
init {
viewModelScope.launch {
prefs.passKey.collect { currentPassKey = it }
}
}
fun updatePassKey(newKey: String) {
viewModelScope.launch {
prefs.setPassKey(newKey)
}
}
fun setAutoMerge(on: Boolean) {
_autoMerge.value = on
syncPrefs.edit { putBoolean("auto_merge", on) }
peer.refreshMeta()
autoMergeTick()
}
fun noteActivity() {
if (!peer.autoMergeReady()) return
idleJob?.cancel()
idleJob = viewModelScope.launch {
// real-time sync delay
delay(3000)
autoMergeTick()
}
}
private fun autoMergeTick() {
if (!peer.autoMergeReady()) return
viewModelScope.launch(Dispatchers.IO) {
val note = dao.getNoteSync()
val local = note?.lastModified ?: 0L
if (local <= _peerLastModified.value) return@launch
doSend(note?.content ?: "", local)
}
}
private val peer = PeerManager(
getLocalPassKey = { currentPassKey },
getLocalLastModified = { dao.getNoteSync()?.lastModified ?: 0L },
getAutoMerge = { _autoMerge.value },
onPeerMeta = { peerDate -> _peerLastModified.value = peerDate },
onMessageReceived = { cleanMessage, peerDate ->
viewModelScope.launch(Dispatchers.IO) {
val stamp = if (peerDate > 0L) peerDate else System.currentTimeMillis()
dao.save(NoteEntity(id = 1, content = cleanMessage, lastModified = stamp))
NoteWidgetProvider.triggerUpdate(appContext)
syncPrefs.edit {
putLong("sync_last_stamp", System.currentTimeMillis())
}
withContext(Dispatchers.Main) {
_syncStatus.value = "Received"
_syncEvent.value = System.currentTimeMillis()
noteActivity()
}
}
},
onLinkReady = { autoMergeTick() },
onStatusChanged = { status, isConnected ->
viewModelScope.launch {
_syncStatus.value = status
_isLinkUp.value = isConnected
}
}
)
init {
peer.startListener()
}
fun trySendNoteText(route: String, peerHost: String, onOlderThanDestination: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val host = if (route == "USB") "127.0.0.1" else peerHost
if (!peer.dial(host, 35691)) return@launch
val note = dao.getNoteSync()
val localDate = note?.lastModified ?: 0L
val peerDate = _peerLastModified.value
if (peerDate != 0L && localDate < peerDate) {
withContext(Dispatchers.Main) { onOlderThanDestination() }
} else {
doSend(note?.content ?: "", localDate)
}
}
}
fun confirmSendNoteText() {
if (!peer.isLinkUp()) return
viewModelScope.launch(Dispatchers.IO) {
val note = dao.getNoteSync()
doSend(note?.content ?: "", note?.lastModified ?: 0L)
}
}
private fun doSend(text: String, date: Long) {
peer.sendTextMessage(text, currentPassKey, date, onSuccess = {
_syncStatus.value = "Sent"
})
}
val editorStyle = prefs.settings.map { settings ->
TextStyle(
fontSize = settings.editorFontSize.sp,
fontFamily = FontFamily.Monospace,
lineHeight = (settings.editorFontSize * settings.lineHeight).sp,
color = parseColor(settings.textColor)
)
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
TextStyle(
fontSize = WedgeConfig.FONT_SIZE_DEFAULT.sp,
fontFamily = FontFamily.Monospace,
lineHeight = (WedgeConfig.FONT_SIZE_DEFAULT * WedgeConfig.LINE_HEIGHT_DEFAULT).sp,
color = parseColor(WedgeConfig.TEXT_COLOR_DEFAULT)
)
)
override fun onCleared() {
super.onCleared()
peer.stop()
}
}
class MainViewModelFactory(
private val dao: NoteDao,
private val prefs: PreferenceManager,
private val context: Context
) : androidx.lifecycle.ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(dao, prefs, context) as T
}
}