From 4abfbc3e9ca06a9a1cd690f015ceac1db83845d8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 01:27:17 +0200 Subject: [PATCH] Open Android file browser from channels and members --- .../main/java/com/utun/chat/MainActivity.kt | 48 +++++++++++++++++-- .../com/utun/chat/ui/screens/ChatScreen.kt | 7 +++ .../utun/chat/ui/screens/MemberListScreen.kt | 4 ++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index 03b33f5d..c2c8b247 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -56,6 +56,8 @@ import com.utun.chat.data.LogManager import com.utun.chat.data.Message import com.utun.chat.data.ConversationTarget import com.utun.chat.data.NativeLib +import com.utun.chat.data.FileLocation +import com.utun.chat.data.MemberFilesProtocol import com.utun.chat.data.RadioOverlayService import com.utun.chat.data.RadioAudioEngine import com.utun.chat.data.RadioPttSource @@ -64,14 +66,16 @@ import com.utun.chat.ui.theme.VibeChatTheme import com.utun.chat.ui.components.ActiveCallBanner import com.utun.chat.ui.screens.* import com.utun.chat.viewmodel.ChatViewModel +import com.utun.chat.viewmodel.MemberFilesViewModel import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collect class MainActivity : AppCompatActivity() { - enum class Screen { ChannelList, Chat, MemberList, Settings, Logs, VideoRecord, PhotoRecord, Call, Dm } + enum class Screen { ChannelList, Chat, MemberList, Files, Settings, Logs, VideoRecord, PhotoRecord, Call, Dm } private var overLock = false private var inputScreen = "setup" + private var requestedFiles by mutableStateOf(null) override fun dispatchTouchEvent(event: MotionEvent): Boolean { val started = SystemClock.uptimeMillis() try { @@ -141,6 +145,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + readFilesIntent(intent) lifecycleScope.launch { ChatApplication.instance.configProvider.radioVolumePttEnabled.collect { enabled -> if (!enabled) releaseVolumePtt("option-disabled") @@ -189,6 +194,7 @@ class MainActivity : AppCompatActivity() { return@VibeChatTheme } val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) + val filesVm: MemberFilesViewModel = viewModel(factory = MemberFilesViewModel.Factory) /* Pause GUI polling (health-check, recording/playback timers) when backgrounded, refresh statuses on resume. */ @@ -198,6 +204,17 @@ class MainActivity : AppCompatActivity() { } var screen by rememberSaveable { mutableStateOf(Screen.ChannelList) } + var filesReturnScreen by rememberSaveable { mutableStateOf(Screen.Chat) } + val fileChannels by vm.channels.collectAsState() + LaunchedEffect(requestedFiles, fileChannels) { + requestedFiles?.let { target -> + val channel = fileChannels.find { it.id == target.group.toULong().toString() } + if (channel != null) { + requestedFiles = null; vm.selectChannel(channel); filesVm.openLocation(target) + filesReturnScreen = Screen.Chat; screen = Screen.Files + } + } + } var showJoinDialog by rememberSaveable { mutableStateOf(false) } var inviteChannelId by rememberSaveable { mutableStateOf(null) } var videoMessage by remember { mutableStateOf(null) } @@ -227,7 +244,7 @@ class MainActivity : AppCompatActivity() { inputScreen = screen.name LogManager.addLog("INFO", "MAIN", "UI screen=$screen") val unavailable = when (screen) { - Screen.Chat, Screen.MemberList, Screen.PhotoRecord -> vm.currentChannel.value == null + Screen.Chat, Screen.MemberList, Screen.Files, Screen.PhotoRecord -> vm.currentChannel.value == null Screen.Dm -> vm.currentDm.value == null Screen.VideoRecord -> videoTarget == null Screen.Call -> callState == null @@ -291,7 +308,7 @@ class MainActivity : AppCompatActivity() { } } - BackHandler(enabled = screen != Screen.ChannelList) { + BackHandler(enabled = screen != Screen.ChannelList && screen != Screen.Files) { when (screen) { Screen.MemberList -> screen = Screen.Chat Screen.VideoRecord -> screen = if (videoTarget is ConversationTarget.Pm) Screen.Dm else Screen.Chat @@ -368,6 +385,9 @@ class MainActivity : AppCompatActivity() { viewModel = vm, onSendMessage = { vm.sendMessage(it) }, onMembersClick = { screen = Screen.MemberList }, + onFiles = { owner -> + filesVm.open(channel.id, owner); filesReturnScreen = Screen.Chat; screen = Screen.Files + }, onInvite = { showInvite(channel) }, onBack = { screen = Screen.ChannelList }, onPlayVideo = { msg -> videoMessage = msg }, @@ -393,10 +413,17 @@ class MainActivity : AppCompatActivity() { members = members, selectedMember = selectedMember, viewModel = vm, + onFiles = { owner -> + filesVm.open(channel.id, owner); filesReturnScreen = Screen.MemberList; screen = Screen.Files + }, onBack = { screen = Screen.Chat } ) } } + Screen.Files -> { + val channel = vm.currentChannel.collectAsState().value + if (channel != null) MemberFilesScreen(channel, filesVm) { screen = filesReturnScreen } + } Screen.Dm -> { val dmMessages by vm.dmMessages.collectAsState() val dm = vm.currentDm.collectAsState().value @@ -552,6 +579,7 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + readFilesIntent(intent) if (intent.getBooleanExtra(MicrophonePermission.EXTRA_REQUEST, false)) { intent.removeExtra(MicrophonePermission.EXTRA_REQUEST) requestMicrophone("PTT") { @@ -568,6 +596,20 @@ class MainActivity : AppCompatActivity() { private fun incomingCallId(intent: Intent?): Long = intent?.getLongExtra(IncomingCallManager.EXTRA_CALL_ID, 0L) ?: 0L + private fun readFilesIntent(intent: Intent) { + val group = intent.getLongExtra("files_group", 0) + if (group == 0L) return + val server = intent.getLongExtra("files_server", 0) + val owner = intent.getLongExtra("files_owner", 0) + val path = intent.getStringExtra("files_path") ?: "" + intent.removeExtra("files_group") + if (server == 0L || !MemberFilesProtocol.validPath(path) || owner == 0L && path.isNotEmpty()) { + LogManager.addLog("ERROR", "Files", "Invalid notification target group=$group server=$server owner=$owner path=$path"); return + } + requestedFiles = FileLocation(group, server, owner, path) + LogManager.addLog("INFO", "Files", "Notification opened location=$requestedFiles") + } + /** Показать активити поверх блокировки и включить экран (входящий звонок). */ private fun showOverLockScreen() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index a47114ff..32dca277 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -1,5 +1,7 @@ package com.utun.chat.ui.screens +import com.utun.chat.data.NativeLib + import com.utun.chat.R import androidx.compose.ui.res.stringResource import android.os.SystemClock @@ -80,6 +82,7 @@ fun ChatScreen( viewModel: ChatViewModel, onSendMessage: (String) -> Unit, onMembersClick: () -> Unit, + onFiles: (Long) -> Unit = {}, onInvite: () -> Unit, onBack: () -> Unit, onPlayVideo: (Message) -> Unit = {}, @@ -181,6 +184,10 @@ fun ChatScreen( Box { IconButton(onClick = { menu = true }) { Icon(Icons.Filled.MoreVert, stringResource(R.string.options)) } DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) { + DropdownMenuItem(text = { Text(stringResource(R.string.files_title)) }, + onClick = { menu = false; onFiles(0) }) + DropdownMenuItem(text = { Text(stringResource(R.string.files_mine)) }, + onClick = { menu = false; onFiles(NativeLib.getMyNodeId()) }) DropdownMenuItem(text = { Text(stringResource(R.string.poll_create)) }, onClick = { menu = false; createPoll = true }) DropdownMenuItem(text = { Text(stringResource(R.string.invite)) }, leadingIcon = { Icon(Icons.Filled.Link, null) }, onClick = { menu = false; onInvite() }) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index ac607d0f..eab1fadd 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -59,6 +59,7 @@ fun MemberListScreen( members: List, selectedMember: MemberDetail?, viewModel: ChatViewModel, + onFiles: (Long) -> Unit = {}, onBack: () -> Unit ) { var showEditDialog by remember { mutableStateOf(false) } @@ -124,6 +125,7 @@ fun MemberListScreen( localSocketStates = localSockets, isSelf = member?.isSelf == true, canEdit = member != null, + onFiles = { onFiles(detail.nodeId) }, onConnect = { Log.i("utun-gui", "Member detail connect: channel=${channel.id} node=${detail.nodeId}") viewModel.connectMemberNode(channel.id, detail.nodeId) @@ -291,6 +293,7 @@ private fun MemberDetailCard( localSocketStates: List, isSelf: Boolean = false, canEdit: Boolean, + onFiles: () -> Unit, onConnect: () -> Unit, onEdit: () -> Unit ) { @@ -363,6 +366,7 @@ private fun MemberDetailCard( } if (!isSelf) TextButton(onClick = onConnect, enabled = !detail.directUp) { Text(stringResource(R.string.connect)) } TextButton(onClick = onEdit, enabled = canEdit) { Text(stringResource(R.string.edit)) } + TextButton(onClick = onFiles) { Text(stringResource(R.string.files_member)) } } Spacer(Modifier.height(4.dp))