Add analytics tab view analytics
This commit is contained in:
@@ -4,6 +4,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.plugin.compose"
|
id "org.jetbrains.kotlin.plugin.compose"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "ai.multica.app"
|
namespace "ai.multica.app"
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
@@ -46,6 +47,12 @@ android {
|
|||||||
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\""
|
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
Vendored
@@ -1,11 +1,13 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:name=".MulticaApplication"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
|||||||
@@ -359,8 +359,10 @@ private data class InboxIssueContext(
|
|||||||
|
|
||||||
private data class PilotIssueDetailData(
|
private data class PilotIssueDetailData(
|
||||||
val issue: Models.Issue,
|
val issue: Models.Issue,
|
||||||
|
val parentIssue: Models.Issue?,
|
||||||
val comments: List<Models.Comment>,
|
val comments: List<Models.Comment>,
|
||||||
val runs: List<Models.AgentTask>,
|
val runs: List<Models.AgentTask>,
|
||||||
|
val projects: List<Models.Project>,
|
||||||
val members: List<Models.Member>,
|
val members: List<Models.Member>,
|
||||||
val agents: List<Models.Agent>,
|
val agents: List<Models.Agent>,
|
||||||
val issueLabels: List<Models.IssueLabel>,
|
val issueLabels: List<Models.IssueLabel>,
|
||||||
@@ -1591,6 +1593,7 @@ private fun ComposePilotShell(
|
|||||||
chatSession != null ||
|
chatSession != null ||
|
||||||
settingsPage != null
|
settingsPage != null
|
||||||
|
|
||||||
|
|
||||||
BackHandler(enabled = secondaryScreenOpen) {
|
BackHandler(enabled = secondaryScreenOpen) {
|
||||||
when {
|
when {
|
||||||
issueFormOpen -> {
|
issueFormOpen -> {
|
||||||
@@ -1971,6 +1974,7 @@ private fun PilotTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PilotSearchPage(
|
private fun PilotSearchPage(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
@@ -7569,6 +7573,68 @@ private fun IssueDetailPriorityMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IssueDetailParentIssueLink(
|
||||||
|
parentIssue: Models.Issue,
|
||||||
|
zh: Boolean,
|
||||||
|
onOpen: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.clickable(onClick = onOpen)
|
||||||
|
.semantics { contentDescription = "Issue Detail Parent Issue Link" },
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = MulticaColors.Surface.copy(alpha = 0.86f),
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(0.75.dp, MulticaColors.Border.copy(alpha = 0.5f)),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 13.dp, vertical = 11.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = issueStatusIcon(parentIssue.status),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MulticaColors.Accent,
|
||||||
|
modifier = Modifier.size(17.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (zh) "子 Issue of" else "Sub-issue of",
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontSize = MulticaTypeScale.Caption,
|
||||||
|
lineHeight = 15.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
color = MulticaColors.Muted,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = listOf(parentIssue.identifier, parentIssue.title).filter { it.isNotBlank() }.joinToString(" "),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
fontSize = MulticaTypeScale.Body,
|
||||||
|
lineHeight = 19.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
color = MulticaColors.Text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MulticaColors.Muted,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IssueDetailHeroCard(
|
private fun IssueDetailHeroCard(
|
||||||
issue: Models.Issue,
|
issue: Models.Issue,
|
||||||
@@ -8869,6 +8935,7 @@ private fun IssueCommentCard(
|
|||||||
members = members,
|
members = members,
|
||||||
agents = agents,
|
agents = agents,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
|
attachments = comment.attachments,
|
||||||
onIssueIdClick = onIssueIdClick,
|
onIssueIdClick = onIssueIdClick,
|
||||||
onIssueReferenceClick = onIssueReferenceClick,
|
onIssueReferenceClick = onIssueReferenceClick,
|
||||||
)
|
)
|
||||||
@@ -9270,7 +9337,9 @@ private fun PilotIssueDetail(
|
|||||||
return api.agentRuns(requestedIssueId, requestedWorkspaceId)
|
return api.agentRuns(requestedIssueId, requestedWorkspaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun projects(): List<Models.Project> = emptyList()
|
override fun projects(): List<Models.Project> = runCatching {
|
||||||
|
api.projects(workspaceId, 100, 0).items
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
|
||||||
override fun members(): List<Models.Member> = api.members(workspaceId)
|
override fun members(): List<Models.Member> = api.members(workspaceId)
|
||||||
|
|
||||||
@@ -9283,11 +9352,13 @@ private fun PilotIssueDetail(
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
val initialData = PilotIssueDetailData(
|
val initialData = PilotIssueDetailData(
|
||||||
core.issue,
|
issue = core.issue,
|
||||||
core.comments,
|
parentIssue = null,
|
||||||
core.runs,
|
comments = core.comments,
|
||||||
core.members,
|
runs = core.runs,
|
||||||
core.agents,
|
projects = core.projects,
|
||||||
|
members = core.members,
|
||||||
|
agents = core.agents,
|
||||||
issueLabels = emptyList(),
|
issueLabels = emptyList(),
|
||||||
allLabels = emptyList(),
|
allLabels = emptyList(),
|
||||||
subscribers = emptyList(),
|
subscribers = emptyList(),
|
||||||
@@ -9304,11 +9375,23 @@ private fun PilotIssueDetail(
|
|||||||
val subscribers = runCatching { api.issueSubscribers(workspaceId, issueId) }.getOrElse { emptyList() }
|
val subscribers = runCatching { api.issueSubscribers(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||||
val attachments = runCatching { api.issueAttachments(workspaceId, issueId) }.getOrElse { emptyList() }
|
val attachments = runCatching { api.issueAttachments(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||||
val children = runCatching { api.childIssues(workspaceId, issueId) }.getOrElse { emptyList() }
|
val children = runCatching { api.childIssues(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||||
|
val parentIssue = core.issue.parentIssueId?.takeIf { it.isNotBlank() }?.let { parentIssueId ->
|
||||||
|
runCatching { api.issue(parentIssueId, workspaceId) }.getOrNull()
|
||||||
|
}
|
||||||
|
val projectId = core.issue.projectId?.takeIf { it.isNotBlank() }
|
||||||
|
val projects = if (projectId != null && core.projects.none { it.id == projectId }) {
|
||||||
|
val issueProject = runCatching { api.project(workspaceId, projectId) }.getOrNull()
|
||||||
|
if (issueProject != null) core.projects + issueProject else core.projects
|
||||||
|
} else {
|
||||||
|
core.projects
|
||||||
|
}
|
||||||
val usage = runCatching { api.issueUsage(workspaceId, issueId) }.getOrNull()
|
val usage = runCatching { api.issueUsage(workspaceId, issueId) }.getOrNull()
|
||||||
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
|
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
|
||||||
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
|
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
|
||||||
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
|
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||||
initialData.copy(
|
initialData.copy(
|
||||||
|
parentIssue = parentIssue,
|
||||||
|
projects = projects,
|
||||||
issueLabels = issueLabels,
|
issueLabels = issueLabels,
|
||||||
allLabels = allLabels,
|
allLabels = allLabels,
|
||||||
subscribers = subscribers,
|
subscribers = subscribers,
|
||||||
@@ -9761,6 +9844,10 @@ private fun PilotIssueDetail(
|
|||||||
val WEB_SSOT_EXECUTION_LOG_SECTION = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/execution-log-section.tsx"
|
val WEB_SSOT_EXECUTION_LOG_SECTION = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/execution-log-section.tsx"
|
||||||
val WEB_SSOT_PULL_REQUEST_LIST = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/pull-request-list.tsx"
|
val WEB_SSOT_PULL_REQUEST_LIST = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/pull-request-list.tsx"
|
||||||
val WEB_SSOT_TERMINATE_TASK_CONFIRM_DIALOG = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/terminate-task-confirm-dialog.tsx"
|
val WEB_SSOT_TERMINATE_TASK_CONFIRM_DIALOG = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/terminate-task-confirm-dialog.tsx"
|
||||||
|
val projectName = data.projects.firstOrNull { it.id == data.issue.projectId }?.name
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: data.issue.projectId?.takeIf { it.isNotBlank() }?.let { Models.shortId(it) }
|
||||||
|
?: if (zh) "无项目" else "No project"
|
||||||
|
|
||||||
PilotListPage(
|
PilotListPage(
|
||||||
title = "${data.issue.identifier} · ${Models.statusLabel(data.issue.status, zh)}",
|
title = "${data.issue.identifier} · ${Models.statusLabel(data.issue.status, zh)}",
|
||||||
@@ -9819,6 +9906,15 @@ private fun PilotIssueDetail(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
data.parentIssue?.let { parentIssue ->
|
||||||
|
item {
|
||||||
|
IssueDetailParentIssueLink(
|
||||||
|
parentIssue = parentIssue,
|
||||||
|
zh = zh,
|
||||||
|
onOpen = { onOpenIssue(parentIssue.id) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
IssueDetailHeroCard(
|
IssueDetailHeroCard(
|
||||||
issue = data.issue,
|
issue = data.issue,
|
||||||
@@ -9833,7 +9929,7 @@ private fun PilotIssueDetail(
|
|||||||
issue = data.issue,
|
issue = data.issue,
|
||||||
issueLabels = data.issueLabels,
|
issueLabels = data.issueLabels,
|
||||||
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
||||||
project = data.issue.projectId?.let { Models.shortId(it) } ?: if (zh) "无项目" else "No project",
|
project = projectName,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -9871,6 +9967,7 @@ private fun PilotIssueDetail(
|
|||||||
members = data.members,
|
members = data.members,
|
||||||
agents = data.agents,
|
agents = data.agents,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
|
attachments = data.attachments,
|
||||||
onIssueIdClick = { onOpenIssue(it) },
|
onIssueIdClick = { onOpenIssue(it) },
|
||||||
onIssueReferenceClick = { openIssueReference(it) },
|
onIssueReferenceClick = { openIssueReference(it) },
|
||||||
)
|
)
|
||||||
@@ -11430,6 +11527,7 @@ private fun MarkdownBlock(
|
|||||||
members: List<Models.Member> = emptyList(),
|
members: List<Models.Member> = emptyList(),
|
||||||
agents: List<Models.Agent> = emptyList(),
|
agents: List<Models.Agent> = emptyList(),
|
||||||
currentUser: Models.User? = null,
|
currentUser: Models.User? = null,
|
||||||
|
attachments: List<Models.Attachment> = emptyList(),
|
||||||
onIssueIdClick: ((String) -> Unit)? = null,
|
onIssueIdClick: ((String) -> Unit)? = null,
|
||||||
onIssueReferenceClick: ((String) -> Unit)? = null,
|
onIssueReferenceClick: ((String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
@@ -11462,11 +11560,18 @@ private fun MarkdownBlock(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectionContainer(
|
val segments = remember(markdown, attachments) { parseMarkdownImageSegments(markdown, attachments) }
|
||||||
|
val markdownContext = LocalContext.current
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.semantics { contentDescription = "Selectable Markdown Block" },
|
.semantics { contentDescription = "Selectable Markdown Block" },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
|
segments.forEach { segment ->
|
||||||
|
when (segment) {
|
||||||
|
is MarkdownTextSegment -> {
|
||||||
|
SelectionContainer {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
@@ -11477,7 +11582,7 @@ private fun MarkdownBlock(
|
|||||||
MarkdownRenderer.render(
|
MarkdownRenderer.render(
|
||||||
view.context,
|
view.context,
|
||||||
view,
|
view,
|
||||||
markdown,
|
segment.text,
|
||||||
textColor,
|
textColor,
|
||||||
mutedColor,
|
mutedColor,
|
||||||
borderColor,
|
borderColor,
|
||||||
@@ -11486,6 +11591,144 @@ private fun MarkdownBlock(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
is MarkdownImageSegment -> {
|
||||||
|
MulticaMarkdownImage(
|
||||||
|
image = segment,
|
||||||
|
onOpen = { url -> openExternalUrl(markdownContext, url) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface MarkdownImageAwareSegment
|
||||||
|
|
||||||
|
private data class MarkdownTextSegment(val text: String) : MarkdownImageAwareSegment
|
||||||
|
|
||||||
|
private data class MarkdownImageSegment(
|
||||||
|
val alt: String,
|
||||||
|
val url: String,
|
||||||
|
) : MarkdownImageAwareSegment
|
||||||
|
|
||||||
|
private val MARKDOWN_IMAGE_LINE_REGEX = Regex("""^!\[([^\]]*)]\(([^)\s]+)(?:\s+"[^"]*")?\)$""")
|
||||||
|
|
||||||
|
private fun parseMarkdownImageSegments(
|
||||||
|
markdown: String,
|
||||||
|
attachments: List<Models.Attachment>,
|
||||||
|
): List<MarkdownImageAwareSegment> {
|
||||||
|
val segments = mutableListOf<MarkdownImageAwareSegment>()
|
||||||
|
val text = StringBuilder()
|
||||||
|
|
||||||
|
fun flushText() {
|
||||||
|
val value = text.toString().trim('\n')
|
||||||
|
if (value.isNotBlank()) segments += MarkdownTextSegment(value)
|
||||||
|
text.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown.lines().forEach { line ->
|
||||||
|
val match = MARKDOWN_IMAGE_LINE_REGEX.matchEntire(line.trim())
|
||||||
|
if (match != null) {
|
||||||
|
flushText()
|
||||||
|
val alt = clean(match.groupValues[1]).ifBlank { "image" }
|
||||||
|
val target = clean(match.groupValues[2]).trim('<', '>')
|
||||||
|
segments += MarkdownImageSegment(
|
||||||
|
alt = alt,
|
||||||
|
url = resolveMarkdownAttachmentImageUrl(target, alt, attachments),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (text.isNotEmpty()) text.append('\n')
|
||||||
|
text.append(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushText()
|
||||||
|
return segments.ifEmpty { listOf(MarkdownTextSegment(markdown)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveMarkdownAttachmentImageUrl(
|
||||||
|
target: String,
|
||||||
|
alt: String,
|
||||||
|
attachments: List<Models.Attachment>,
|
||||||
|
): String {
|
||||||
|
val normalizedTarget = clean(target)
|
||||||
|
val match = attachments.firstOrNull { attachment ->
|
||||||
|
val url = clean(attachment.url)
|
||||||
|
val downloadUrl = clean(attachment.downloadUrl)
|
||||||
|
val filename = clean(attachment.filename)
|
||||||
|
url == normalizedTarget ||
|
||||||
|
downloadUrl == normalizedTarget ||
|
||||||
|
filename.equals(alt, ignoreCase = true)
|
||||||
|
}
|
||||||
|
return clean(match?.downloadUrl).ifBlank {
|
||||||
|
clean(match?.url).ifBlank { normalizedTarget }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MulticaMarkdownImage(
|
||||||
|
image: MarkdownImageSegment,
|
||||||
|
onOpen: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val imageUrl = remember(image.url) { absoluteMarkdownImageUrl(image.url) }
|
||||||
|
val authStore = remember(context) { AuthStore(context.applicationContext) }
|
||||||
|
val cookieHeader = remember(authStore) { authStore.cloudFrontCookieHeader() }
|
||||||
|
val token = remember(authStore) { authStore.token().orEmpty() }
|
||||||
|
val imageRequest = remember(context, imageUrl, cookieHeader, token) {
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.crossfade(true)
|
||||||
|
.apply {
|
||||||
|
if (token.isNotBlank() && imageUrl.startsWith(BuildConfig.MULTICA_API_BASE_URL)) {
|
||||||
|
addHeader("Authorization", "Bearer $token")
|
||||||
|
}
|
||||||
|
if (cookieHeader.isNotBlank() && imageUrl.contains("static.multica.ai")) {
|
||||||
|
addHeader("Cookie", cookieHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.clickable { if (imageUrl.isNotBlank()) onOpen(imageUrl) }
|
||||||
|
.semantics { contentDescription = "Markdown Image ${image.alt}" },
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = MulticaColors.Surface.copy(alpha = 0.9f),
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(0.75.dp, MulticaColors.Border.copy(alpha = 0.48f)),
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageRequest,
|
||||||
|
contentDescription = image.alt,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 140.dp, max = 420.dp)
|
||||||
|
.background(MulticaColors.SurfaceElevated.copy(alpha = 0.35f))
|
||||||
|
.padding(6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun absoluteMarkdownImageUrl(url: String): String {
|
||||||
|
val value = clean(url)
|
||||||
|
return if (value.startsWith("/")) {
|
||||||
|
BuildConfig.MULTICA_API_BASE_URL.trimEnd('/') + value
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openExternalUrl(context: Context, url: String) {
|
||||||
|
val target = clean(url)
|
||||||
|
if (target.isBlank()) return
|
||||||
|
runCatching {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(target)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -20191,24 +20434,33 @@ private fun PilotRuntimesSettings(api: ApiClient, workspaceId: String, zh: Boole
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.semantics { contentDescription = "Runtime Web Table Header" }
|
.semantics { contentDescription = "Runtime Web Table Header Aligned Columns" }
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
.padding(horizontal = RuntimeListHorizontalPadding, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(RuntimeListColumnGap),
|
||||||
) {
|
) {
|
||||||
|
Spacer(Modifier.width(RuntimeListProviderColumnWidth))
|
||||||
Text(
|
Text(
|
||||||
if (zh) "Runtime" else "Runtime",
|
if (zh) "Runtime" else "Runtime",
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f, fill = true),
|
||||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
|
||||||
color = MulticaColors.TextTertiary,
|
color = MulticaColors.TextTertiary,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
if (zh) "健康" else "Health",
|
if (zh) "健康" else "Health",
|
||||||
modifier = Modifier.width(88.dp),
|
modifier = Modifier.width(RuntimeListHealthColumnWidth),
|
||||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
|
||||||
color = MulticaColors.TextTertiary,
|
color = MulticaColors.TextTertiary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(42.dp))
|
Text(
|
||||||
|
if (zh) "版本" else "Version",
|
||||||
|
modifier = Modifier.width(RuntimeListMetaColumnWidth),
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MulticaColors.TextTertiary,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(RuntimeListActionColumnWidth))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20358,6 +20610,13 @@ private fun RuntimesWebNoMatchesState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val RuntimeListProviderColumnWidth = 34.dp
|
||||||
|
private val RuntimeListHealthColumnWidth = 72.dp
|
||||||
|
private val RuntimeListMetaColumnWidth = 76.dp
|
||||||
|
private val RuntimeListActionColumnWidth = 36.dp
|
||||||
|
private val RuntimeListHorizontalPadding = 10.dp
|
||||||
|
private val RuntimeListColumnGap = 8.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RuntimeWebDenseRow(
|
private fun RuntimeWebDenseRow(
|
||||||
runtime: Models.Runtime,
|
runtime: Models.Runtime,
|
||||||
@@ -20393,11 +20652,15 @@ private fun RuntimeWebDenseRow(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp))
|
.border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp))
|
||||||
.clickable(onClick = onOpen)
|
.clickable(onClick = onOpen)
|
||||||
.padding(horizontal = 12.dp, vertical = 9.dp),
|
.padding(horizontal = RuntimeListHorizontalPadding, vertical = 9.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(RuntimeListColumnGap),
|
||||||
) {
|
) {
|
||||||
RuntimeProviderMark(provider = runtime.provider, runtimeMode = runtime.runtimeMode)
|
RuntimeProviderMark(
|
||||||
|
provider = runtime.provider,
|
||||||
|
runtimeMode = runtime.runtimeMode,
|
||||||
|
modifier = Modifier.size(RuntimeListProviderColumnWidth),
|
||||||
|
)
|
||||||
RuntimeRowNameBlock(
|
RuntimeRowNameBlock(
|
||||||
baseName = nameParts.first,
|
baseName = nameParts.first,
|
||||||
hostname = nameParts.second,
|
hostname = nameParts.second,
|
||||||
@@ -20406,11 +20669,11 @@ private fun RuntimeWebDenseRow(
|
|||||||
deviceInfo = runtime.deviceInfo,
|
deviceInfo = runtime.deviceInfo,
|
||||||
lastSeenAt = runtime.lastSeenAt,
|
lastSeenAt = runtime.lastSeenAt,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f, fill = true),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(88.dp)
|
.width(RuntimeListHealthColumnWidth)
|
||||||
.semantics { contentDescription = "Runtime Health ${runtimeHealthLabel(health, zh)}" },
|
.semantics { contentDescription = "Runtime Health ${runtimeHealthLabel(health, zh)}" },
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -20431,7 +20694,7 @@ private fun RuntimeWebDenseRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.widthIn(min = 72.dp, max = 96.dp),
|
modifier = Modifier.width(RuntimeListMetaColumnWidth),
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
) {
|
) {
|
||||||
@@ -20444,7 +20707,7 @@ private fun RuntimeWebDenseRow(
|
|||||||
onClick = { onExpandedChange(true) },
|
onClick = { onExpandedChange(true) },
|
||||||
tone = if (expanded) MulticaButtonTone.Primary else MulticaButtonTone.Ghost,
|
tone = if (expanded) MulticaButtonTone.Primary else MulticaButtonTone.Ghost,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(RuntimeListActionColumnWidth)
|
||||||
.semantics { contentDescription = "Runtime Actions ${runtime.name}" },
|
.semantics { contentDescription = "Runtime Actions ${runtime.name}" },
|
||||||
)
|
)
|
||||||
MulticaCupertinoActionSheet(
|
MulticaCupertinoActionSheet(
|
||||||
@@ -20537,7 +20800,7 @@ private fun RuntimeRowNameBlock(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.semantics { contentDescription = "Runtime Row Name Block ${baseName.ifBlank { hostname ?: "" }}" },
|
modifier = modifier.semantics { contentDescription = "Runtime Row Name Expanded Layout ${baseName.ifBlank { hostname ?: "" }}" },
|
||||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -20545,7 +20808,7 @@ private fun RuntimeRowNameBlock(
|
|||||||
modifier = Modifier.semantics { contentDescription = "Runtime Row Base Name ${baseName.ifBlank { "untitled" }}" },
|
modifier = Modifier.semantics { contentDescription = "Runtime Row Base Name ${baseName.ifBlank { "untitled" }}" },
|
||||||
style = MaterialTheme.typography.titleSmall.copy(fontSize = 14.sp, lineHeight = 18.sp, fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleSmall.copy(fontSize = 14.sp, lineHeight = 18.sp, fontWeight = FontWeight.SemiBold),
|
||||||
color = MulticaColors.Text,
|
color = MulticaColors.Text,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -21323,7 +21586,7 @@ private fun RuntimeDetailWebDenseRow(
|
|||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
),
|
),
|
||||||
color = MulticaColors.Text,
|
color = MulticaColors.Text,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
if (!subtitle.isNullOrBlank()) {
|
if (!subtitle.isNullOrBlank()) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ai.multica.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
public final class MulticaApplication extends Application {
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user