package com.dadao.task import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.dadao.task.data.Api import com.dadao.task.data.AppRelease import com.dadao.task.data.AppUpdater import com.dadao.task.data.TokenStore import com.dadao.task.data.Workspace import androidx.compose.foundation.layout.Box import com.dadao.task.ui.BoardScreen import com.dadao.task.ui.DadaoTheme import com.dadao.task.ui.EditRequest import com.dadao.task.ui.HomeScreen import com.dadao.task.ui.KnowledgeScreen import com.dadao.task.ui.LoginScreen import com.dadao.task.ui.MembersScreen import com.dadao.task.ui.NotificationsScreen import com.dadao.task.ui.RichTextEditorScreen import com.dadao.task.ui.TaskListScreen import com.dadao.task.ui.TicketsScreen import com.dadao.task.ui.UpdateDialog import com.dadao.task.ui.WorkspaceScaffold import com.dadao.task.ui.WorkspaceTab class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { DadaoTheme { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { AppRoot(joinUri = intent?.dataString) } } } } } private enum class Screen { Splash, Login, Home, Workspace } /** 从 https://dadaotask.com/join// 里拆出工作区与邀请码。 */ private fun parseJoin(uri: String?): Pair? { if (uri.isNullOrBlank()) return null val parts = uri.trimEnd('/').split("/") val idx = parts.indexOf("join") if (idx < 0 || parts.size < idx + 3) return null val ws = parts[idx + 1] val code = parts[idx + 2] if (ws.length < 10 || code.length < 4) return null return ws to code } @Composable private fun AppRoot(joinUri: String?) { val context = LocalContext.current val tokens = remember { TokenStore(context) } val api = remember { Api(tokens) } var screen by remember { mutableStateOf(if (tokens.isLoggedIn) Screen.Splash else Screen.Login) } var pendingJoin by remember { mutableStateOf(parseJoin(joinUri)) } var current by remember { mutableStateOf(null) } var tab by remember { mutableStateOf(WorkspaceTab.Tasks) } var unread by remember { mutableStateOf(0) } // 通知页标了已读之后要立刻刷新角标:改这个数会触发下面的 LaunchedEffect var unreadRefresh by remember { mutableStateOf(0) } // 打开富文本编辑器时非空;关掉就恢复原来的界面 var editing by remember { mutableStateOf(null) } // 编辑器保存成功的那条:回到工作区后重新打开它的详情,然后清掉 var reopenItem by remember { mutableStateOf(null) } // 上面那条是任务还是知识:决定回哪个页面去重开 var reopenKnowledge by remember { mutableStateOf(false) } // 应用内升级:启动后查一次,查到了就把对话框摆出来 val updater = remember { AppUpdater(context, api) } var update by remember { mutableStateOf(null) } // 有本地令牌就先静默校验一次:令牌过期/被吊销要退回登录页,而不是进了主页再崩。 LaunchedEffect(screen) { if (screen == Screen.Splash) { screen = try { api.me() Screen.Home } catch (e: Exception) { tokens.clear() Screen.Login } } } // 邀请链接:登录之后自动加入,加入完直接进那个工作区。 LaunchedEffect(screen, pendingJoin) { val join = pendingJoin ?: return@LaunchedEffect if (screen != Screen.Home) return@LaunchedEffect runCatching { api.join(join.first, join.second) } .onSuccess { current = it tab = WorkspaceTab.Tasks pendingJoin = null screen = Screen.Workspace } .onFailure { pendingJoin = null } } // 未读数:进工作区、切页面、或通知页标了已读(unreadRefresh)时刷新 LaunchedEffect(current?.id, tab, unreadRefresh) { val ws = current ?: return@LaunchedEffect unread = api.unreadCount(ws.id) } // 版本检查:登录成功后查一次就够,查不到(网络、没发布过包)就当没有新版 LaunchedEffect(screen) { if (screen != Screen.Home && screen != Screen.Workspace) return@LaunchedEffect if (update != null) return@LaunchedEffect update = runCatching { updater.check() }.getOrNull() } /** 手动检查:结果用一句话回给首页显示,查到新版顺便把对话框摆出来。 */ suspend fun checkUpdate(): String = runCatching { updater.checkStrict() }.fold( onSuccess = { release -> if (release == null) { "已经是最新版 ${updater.currentVersionName}" } else { update = release "发现新版本 ${release.versionName}" } }, onFailure = { "检查更新失败:${it.message ?: "网络不通"}" }, ) when (screen) { Screen.Splash -> Unit Screen.Login -> LoginScreen(api = api, onLoggedIn = { screen = Screen.Home }) Screen.Home -> HomeScreen( api = api, versionName = updater.currentVersionName, onCheckUpdate = { checkUpdate() }, onLogout = { tokens.clear() screen = Screen.Login }, onOpenWorkspace = { ws -> current = ws tab = WorkspaceTab.Tasks screen = Screen.Workspace }, ) Screen.Workspace -> { val ws = current val request = editing if (ws == null) { screen = Screen.Home } else if (request != null) { // 编辑器**替换**整页,而不是叠在上面:抽屉/对话框都是独立窗口, // 叠一层会被它们盖住,用户只能看到编辑器的一条边。 RichTextEditorScreen( api = api, workspaceId = ws.id, itemId = request.itemId, initialTitle = request.title, initialBody = request.body, titleEnabled = request.titleEnabled, save = { title, body -> if (request.knowledge) { api.updateKnowledge( workspaceId = ws.id, itemId = request.itemId, version = request.version, title = title, body = body, ) } else { api.updateItem( workspaceId = ws.id, itemId = request.itemId, version = request.version, title = title, body = body, ) } // 保存成功才标记"回到哪条":取消退出不该改变任何界面状态。 reopenItem = request.itemId reopenKnowledge = request.knowledge }, // saved 参数由编辑器给:true 时上面已经设过 reopenItem, // 这里只负责把编辑器收起来,回到工作区后由对应页面重开详情。 onClose = { _ -> editing = null }, ) } else { WorkspaceScaffold( workspace = ws, tab = tab, unread = unread, onTab = { tab = it }, onBackHome = { screen = Screen.Home }, ) { when (tab) { WorkspaceTab.Tasks -> TaskListScreen( api = api, workspace = ws, onEdit = { editing = it }, reopenItem = if (reopenItem != null && !reopenKnowledge) reopenItem else null, onReopened = { reopenItem = null }, ) WorkspaceTab.Tickets -> TicketsScreen( api = api, workspace = ws, focusId = reopenItem, onFocusHandled = { reopenItem = null }, ) WorkspaceTab.Knowledge -> KnowledgeScreen( api = api, workspace = ws, onEdit = { editing = it }, reopenItem = if (reopenItem != null && reopenKnowledge) reopenItem else null, onReopened = { reopenItem = null }, ) WorkspaceTab.Members -> MembersScreen(api = api, workspace = ws) WorkspaceTab.Notifications -> NotificationsScreen( api = api, workspaceId = ws.id, onOpenItem = { itemId, isTicket -> // 通知点开后跳到对应列表并展开那一条;通知本身不带数据,页面自己回源 reopenItem = itemId reopenKnowledge = false tab = if (isTicket) WorkspaceTab.Tickets else WorkspaceTab.Tasks }, onUnreadChanged = { unreadRefresh += 1 }, ) } } } } } // 更新对话框是独立窗口,摆在哪一层都能盖住当前界面 update?.let { release -> UpdateDialog( release = release, currentVersionName = updater.currentVersionName, updater = updater, onLater = { update = null }, ) } }