Add wIRC main scrollback, route services to it, remember window bounds, bump to 0.1.7
wIRC main scrollback - The main window now has a "Server messages" scrollback using the same rendering and stick-to-bottom code as channel windows (extracted from ChannelScreen into a shared MessageScrollback composable). - MOTD, numerics, errors, connection status, user modes, INVITE, ERROR, non-ACTION CTCP requests and notices/messages from services (NickServ, ChanServ, *Serv, Global, ...) and the server go there. Its history persists like any other buffer (history/wirc-main__main.json). - Unread indicator: lines arriving while the main window is unfocused show as "N new" next to the label and in the window title/taskbar. Private messages and /whois - A PM or notice from a real user opens its query window, and reopens it if it was closed. Services and the server no longer open windows. - /whois <nick> sends WHOIS and shows the formatted reply in the window it was typed in, or in wIRC main if that window has since closed. - /me now sends a proper CTCP ACTION (the \x01 wrapper was missing). Window position and size - Main, channel and PM windows remember position and size in window_prefs.json under the existing "<serverId>__<name>" key (main uses "wirc-main__main"). The new windowX/windowY/windowWidth/ windowHeight fields are optional, so older files still load. - A saved position is only used if the window's title strip is on a connected monitor; otherwise, and for windows with no saved position, windows cascade from the primary screen's top-left with a 32dp offset. - window_prefs.json is now written via a temp file + atomic move. Cleanup - Remove the TEMP DEBUG aliasDebug tracing, ServerRepository.upsert printlns and connmgr-debug printlns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
242d7a7c42
commit
807903fe0f
@@ -1,5 +1,7 @@
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -11,7 +13,9 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.isShiftPressed
|
||||
@@ -23,9 +27,12 @@ import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogWindow
|
||||
import androidx.compose.ui.window.rememberDialogState
|
||||
import data.MAIN_BUFFER_KEY
|
||||
import data.ServerConfig
|
||||
import data.ServerRepository
|
||||
import data.WindowDisplaySettings
|
||||
import irc.ConnectionManager
|
||||
import irc.mircPalette
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -33,8 +40,10 @@ import kotlinx.coroutines.launch
|
||||
fun MainScreen(
|
||||
repo: ServerRepository,
|
||||
colorScheme: ColorScheme,
|
||||
displaySettings: WindowDisplaySettings,
|
||||
onOpenChannel: (serverId: String, channelName: String) -> Unit,
|
||||
onExitApp: () -> Unit
|
||||
onExitApp: () -> Unit,
|
||||
onUnreadChange: (Int) -> Unit
|
||||
) {
|
||||
var servers by remember { mutableStateOf(repo.loadAll()) }
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
@@ -44,6 +53,26 @@ fun MainScreen(
|
||||
var openedChannels by remember { mutableStateOf(mapOf<String, List<String>>()) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val buffers by ConnectionManager.buffers.collectAsState()
|
||||
val mainMessages by remember { derivedStateOf { buffers[MAIN_BUFFER_KEY]?.messages ?: emptyList() } }
|
||||
val textColor = mircPalette[displaySettings.textColorMircIndex]
|
||||
|
||||
// Unread indicator: lines that arrived in wIRC main while this window didn't have focus
|
||||
// (e.g. while typing in a channel window). Shown next to the "Server messages" label and,
|
||||
// via onUnreadChange, in the window title / taskbar. Cleared as soon as the window is
|
||||
// focused. Compared by timestamp rather than count so history loaded at startup never
|
||||
// counts as new.
|
||||
val windowFocused = LocalWindowInfo.current.isWindowFocused
|
||||
var lastSeenMillis by remember { mutableStateOf(System.currentTimeMillis()) }
|
||||
val newest = mainMessages.lastOrNull()
|
||||
LaunchedEffect(windowFocused, newest?.seq) {
|
||||
if (windowFocused) {
|
||||
lastSeenMillis = maxOf(lastSeenMillis, newest?.timestampMillis ?: 0L, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
val unread = if (windowFocused) 0 else mainMessages.count { it.timestampMillis > lastSeenMillis }
|
||||
LaunchedEffect(unread) { onUnreadChange(unread) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -125,6 +154,34 @@ fun MainScreen(
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Server messages", style = MaterialTheme.typography.labelMedium)
|
||||
if (unread > 0) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"\u25CF $unread new",
|
||||
color = UNREAD_INDICATOR_COLOR,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
// MOTD, numerics, errors, services/server notices and WHOIS replies whose window
|
||||
// is gone - see ConnectionManager. Same rendering and auto-scroll as a channel window.
|
||||
MessageScrollback(
|
||||
messages = mainMessages,
|
||||
displaySettings = displaySettings,
|
||||
textColor = textColor,
|
||||
isMention = { false },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = displaySettings.backgroundAlpha))
|
||||
.border(1.dp, Color.Gray.copy(alpha = 0.5f))
|
||||
.padding(vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (showAddDialog) {
|
||||
@@ -141,6 +198,8 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private val UNREAD_INDICATOR_COLOR = Color(0xFFFFA500)
|
||||
|
||||
@Composable
|
||||
private fun ServerCard(
|
||||
server: ServerConfig,
|
||||
|
||||
Reference in New Issue
Block a user