|
|
|
|
@ -220,6 +220,23 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
|
|
|
|
|
private val _currentChannel = MutableStateFlow<Channel?>(null) |
|
|
|
|
val currentChannel: StateFlow<Channel?> = _currentChannel |
|
|
|
|
private val _streamSources = MutableStateFlow<List<StreamSource>>(emptyList()) |
|
|
|
|
val streamSources: StateFlow<List<StreamSource>> = _streamSources |
|
|
|
|
private val _streamSourcesFailed = MutableStateFlow(false) |
|
|
|
|
val streamSourcesFailed: StateFlow<Boolean> = _streamSourcesFailed |
|
|
|
|
private val _streamSelection = MutableStateFlow<StreamSource?>(null) |
|
|
|
|
val streamSelection: StateFlow<StreamSource?> = _streamSelection |
|
|
|
|
|
|
|
|
|
fun selectStream(source: StreamSource?) { |
|
|
|
|
_streamSelection.value = source |
|
|
|
|
LogManager.addLog("INFO", "stream", "selected channel=${_currentChannel.value?.id} source=${source?.id} epoch=${source?.epoch}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun watchStreams() { |
|
|
|
|
if (!NativeLib.isReady()) return |
|
|
|
|
val group = if (_foreground.value) _currentChannel.value?.id?.toULongOrNull()?.toLong() ?: 0L else 0L |
|
|
|
|
if (!NativeLib.streamWatch(group)) LogManager.addLog("WARN", "stream", "source watch rejected group=$group") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private val _soundOn = MutableStateFlow(true) |
|
|
|
|
val soundOn: StateFlow<Boolean> = _soundOn |
|
|
|
|
@ -320,6 +337,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
|
|
|
|
|
fun setForeground(fg: Boolean) { |
|
|
|
|
_foreground.value = fg |
|
|
|
|
watchStreams() |
|
|
|
|
if (fg) { |
|
|
|
|
_isStuck.value = false |
|
|
|
|
if (dbReady && repo != null) { |
|
|
|
|
@ -533,8 +551,9 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
if (result == 0) refreshChannels() |
|
|
|
|
} |
|
|
|
|
4 -> { if (repo != null) { LogManager.addLog("INFO", "VM", "CHANNEL_UPDATED — refreshChannels"); refreshChannels() } else { LogManager.addLog("WARN", "VM", "CHANNEL_UPDATED skipped — repo is NULL") } } |
|
|
|
|
12 -> LogManager.addLog("INFO", "VM", "service started") |
|
|
|
|
12 -> { LogManager.addLog("INFO", "VM", "service started"); watchStreams() } |
|
|
|
|
13 -> { |
|
|
|
|
_streamSources.value = emptyList(); _streamSourcesFailed.value = false; _streamSelection.value = null |
|
|
|
|
for (id in submissions.keys.toList()) submissionResult(id, false) |
|
|
|
|
_radioTalkState.value = RadioTalkState() |
|
|
|
|
_radioTalking.value = null |
|
|
|
|
@ -641,6 +660,21 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
val cur = _currentChannel.value |
|
|
|
|
if (cur != null) refreshMembers(cur.id) |
|
|
|
|
} |
|
|
|
|
58 -> { /* STREAM_SOURCES: group:8BE, valid:1, JSON. Старый канал не меняет новую шапку. */ |
|
|
|
|
if (data == null || data.size < 11 || data[8].toInt() !in 0..1) { |
|
|
|
|
LogManager.addLog("ERROR", "stream", "invalid source snapshot bytes=${data?.size}"); return |
|
|
|
|
} |
|
|
|
|
val group = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.BIG_ENDIAN).long |
|
|
|
|
if (_currentChannel.value?.id?.toULongOrNull()?.toLong() != group || !_foreground.value) return |
|
|
|
|
try { |
|
|
|
|
_streamSourcesFailed.value = data[8].toInt() == 0 |
|
|
|
|
_streamSources.value = StreamSource.parse(String(data, 9, data.size - 9, Charsets.UTF_8)) |
|
|
|
|
LogManager.addLog("INFO", "stream", "sources group=$group count=${_streamSources.value.size} valid=${!_streamSourcesFailed.value}") |
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
_streamSources.value = emptyList(); _streamSourcesFailed.value = true |
|
|
|
|
LogManager.addLog("ERROR", "stream", "source snapshot parse group=$group: ${e.message}") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
19 -> { if (repo != null) refreshLocalSockets() } /* LOCAL_SOCKETS */ |
|
|
|
|
23 -> { /* MEMBER_LIST: live-снимок дополняет расширенный JSON-список. */ |
|
|
|
|
if (data == null || data.size < 3) { |
|
|
|
|
@ -1215,6 +1249,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
fun selectChannel(channel: Channel) { |
|
|
|
|
val r = repo ?: return |
|
|
|
|
if (_currentChannel.value?.id != channel.id) { |
|
|
|
|
_streamSources.value = emptyList(); _streamSourcesFailed.value = false; _streamSelection.value = null |
|
|
|
|
_historyAnchor.value = "" |
|
|
|
|
_messageJump.value = null |
|
|
|
|
pendingLatestJump = null |
|
|
|
|
@ -1224,6 +1259,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null |
|
|
|
|
} |
|
|
|
|
_currentChannel.value = channel |
|
|
|
|
watchStreams() |
|
|
|
|
_radioActive.value = (radioChannelId == channel.id) |
|
|
|
|
_radioTalking.value = if (_radioActive.value) _radioTalkState.value.speakers.firstOrNull() else null |
|
|
|
|
_soundOn.value = NativeLib.getSoundOnMessage(channel.id) |
|
|
|
|
@ -1783,6 +1819,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
_channels.value = emptyList() |
|
|
|
|
_messages.value = emptyList() |
|
|
|
|
_currentChannel.value = null |
|
|
|
|
_streamSources.value = emptyList(); _streamSelection.value = null; _streamSourcesFailed.value = false |
|
|
|
|
_generatingKeys.value = false |
|
|
|
|
_localSockets.value = emptyList() |
|
|
|
|
_channelOwner.value = false |
|
|
|
|
|