import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import data.AliasRepository import data.BufferKey import data.MAIN_BUFFER_KEY import data.ServerRepository import data.WindowPrefsRepository import data.storageId import irc.ConnectionManager import ui.ChannelWindow import ui.MainScreen import ui.rememberPersistentWindowState import java.io.File // Material 3's darkColorScheme() defaults to a purple accent; this keeps everything // (OutlinedTextField borders, Button/TextButton labels, etc.) neutral black-and-white // to match the rest of the app's deliberately plain look, same as the Android version. private val neutralDarkScheme = darkColorScheme( primary = Color.White, onPrimary = Color.Black, secondary = Color.White, onSecondary = Color.Black, background = Color.Black, onBackground = Color.White, surface = Color.Black, onSurface = Color.White, outline = Color.White ) // All persistence lives under the user's home directory - the desktop analogue of // Android's app-private filesDir, since a desktop app has no sandboxed storage of its own. private val appRoot = File(System.getProperty("user.home"), ".wirc") fun main() = application { if (!appRoot.exists()) appRoot.mkdirs() ConnectionManager.init(appRoot) // Loaded once here (not per-window) so the alias map is truly global - every // ChannelWindow's command router reads from this same AliasRepository singleton. AliasRepository.init() val repo = remember { ServerRepository(appRoot) } val prefsRepo = remember { WindowPrefsRepository(appRoot) } var openBuffers by remember { mutableStateOf(setOf()) } var mainUnread by remember { mutableStateOf(0) } // ConnectionManager routes WHOIS replies to the window that asked only while that // window is still open, so it needs to know which ones are. LaunchedEffect(openBuffers) { ConnectionManager.openWindows = openBuffers } // Full shutdown: same steps /quit (bare, no args) already runs from a channel window - // disconnect every server, then end the process. Shared here so the "Exit wIRC" button // and every channel window's /quit reuse the exact same logic instead of duplicating it. val shutdownApp: () -> Unit = { ConnectionManager.disconnectAll() exitApplication() } // Desktop OSes don't kill backgrounded network connections for battery reasons the // way Android does, so there's no foreground-service equivalent needed here - // ConnectionManager's sockets just live for as long as this process runs, and this // listener (which the Android foreground service used to own) can live directly at // the application level instead. // // Fires for every private message to or from a real user, so a query window the user // closed reopens when that person messages again. Adding a key that's already open is // a no-op (openBuffers is a set). LaunchedEffect(Unit) { ConnectionManager.queryWindowRequests.collect { key -> openBuffers = openBuffers + key } } // Restored from / saved to window_prefs.json under MAIN_BUFFER_KEY's storage id // ("wirc-main__main"), same as every channel and PM window - see WindowPlacement.kt. val mainWindowState = rememberPersistentWindowState(prefsRepo, MAIN_BUFFER_KEY.storageId(), DpSize(800.dp, 700.dp)) val mainDisplaySettings by prefsRepo.settingsFor(MAIN_BUFFER_KEY.storageId()).collectAsState() // There's no persistent tray/notification surface on this build, so closing the main // server-list window quits the whole app (and every channel window with it) - the // same "closing the primary window ends the session" convention most desktop chat // apps without a tray icon follow. Window( onCloseRequest = ::exitApplication, // The unread count shows in the taskbar too, so new server messages are visible // even when the main window is behind other windows. title = if (mainUnread > 0) "wIRC - Main ($mainUnread new)" else "wIRC - Main", icon = painterResource("icon.png"), state = mainWindowState ) { MaterialTheme(colorScheme = neutralDarkScheme) { Surface { MainScreen( repo = repo, colorScheme = neutralDarkScheme, displaySettings = mainDisplaySettings, onOpenChannel = { serverId, channelName -> openBuffers = openBuffers + BufferKey(serverId, channelName) }, onExitApp = shutdownApp, onUnreadChange = { mainUnread = it } ) } } } openBuffers.forEach { bufferKey -> key(bufferKey) { ChannelWindow( bufferKey = bufferKey, prefsRepo = prefsRepo, colorScheme = neutralDarkScheme, onOpenBuffer = { newKey -> openBuffers = openBuffers + newKey }, onCloseSelf = { openBuffers = openBuffers - bufferKey }, onQuitApp = { exitApplication() } ) } } }