Files
DadaoTask/android/app/src/main/java/com/dadao/task/MainActivity.kt
T
2026-11-10 13:47:56 +08:00

270 lines
11 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
import kotlinx.coroutines.delay
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/<workspaceId>/<code> 里拆出工作区与邀请码。 */
private fun parseJoin(uri: String?): Pair<String, String>? {
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<Workspace?>(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<EditRequest?>(null) }
// 编辑器保存成功的那条:回到工作区后重新打开它的详情,然后清掉
var reopenItem by remember { mutableStateOf<String?>(null) }
// 上面那条是任务还是知识:决定回哪个页面去重开
var reopenKnowledge by remember { mutableStateOf(false) }
// 应用内升级:启动后查一次,查到了就把对话框摆出来
val updater = remember { AppUpdater(context, api) }
var update by remember { mutableStateOf<AppRelease?>(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)时刷新;
// 停留不动时每 30 秒再拉一次,这样"任务被指派/快到期/已逾期"不用切页面也能看到角标。
LaunchedEffect(current?.id, tab, unreadRefresh) {
val ws = current ?: return@LaunchedEffect
while (true) {
unread = api.unreadCount(ws.id)
delay(30_000)
}
}
// 版本检查:登录成功后查一次就够,查不到(网络、没发布过包)就当没有新版
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, itemKind ->
// 通知点开后跳到对应页签并展开那一条;通知本身不带数据,页面自己回源
reopenItem = itemId
reopenKnowledge = itemKind == "knowledge"
tab = when (itemKind) {
"knowledge" -> WorkspaceTab.Knowledge
"ticket" -> WorkspaceTab.Tickets
else -> WorkspaceTab.Tasks
}
},
onUnreadChanged = { unreadRefresh += 1 },
)
}
}
}
}
}
// 更新对话框是独立窗口,摆在哪一层都能盖住当前界面
update?.let { release ->
UpdateDialog(
release = release,
currentVersionName = updater.currentVersionName,
updater = updater,
onLater = { update = null },
)
}
}