Skip to content

Commit 15243de

Browse files
committed
file-preview
1 parent 7b15d8a commit 15243de

File tree

3 files changed

+98
-59
lines changed

3 files changed

+98
-59
lines changed

composeApp/src/wasmJsMain/kotlin/love/forte/simbot/codegen/gen/view/preview/FileContentComponents.kt

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import androidx.compose.foundation.background
44
import androidx.compose.foundation.horizontalScroll
55
import androidx.compose.foundation.layout.*
66
import androidx.compose.foundation.rememberScrollState
7-
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.foundation.text.selection.SelectionContainer
88
import androidx.compose.foundation.verticalScroll
99
import androidx.compose.material.icons.Icons
1010
import androidx.compose.material.icons.filled.ContentCopy
@@ -13,9 +13,9 @@ import androidx.compose.material3.*
1313
import androidx.compose.runtime.*
1414
import androidx.compose.ui.Alignment
1515
import androidx.compose.ui.Modifier
16-
import androidx.compose.ui.draw.clip
1716
import androidx.compose.ui.graphics.Color
18-
import androidx.compose.ui.platform.LocalClipboardManager
17+
import androidx.compose.ui.platform.ClipEntry
18+
import androidx.compose.ui.platform.LocalClipboard
1919
import androidx.compose.ui.text.AnnotatedString
2020
import androidx.compose.ui.text.SpanStyle
2121
import androidx.compose.ui.text.buildAnnotatedString
@@ -25,7 +25,9 @@ import androidx.compose.ui.text.style.TextAlign
2525
import androidx.compose.ui.unit.dp
2626
import androidx.compose.ui.unit.sp
2727
import kotlinx.coroutines.launch
28-
import androidx.compose.foundation.text.selection.SelectionContainer
28+
import org.jetbrains.compose.resources.Font
29+
import simbot_codegen.composeapp.generated.resources.JetBrainsMono_Medium
30+
import simbot_codegen.composeapp.generated.resources.Res
2931

3032
/**
3133
* 文件内容预览组件
@@ -126,11 +128,11 @@ private fun FileContentDisplay(content: FileContent) {
126128
Column(modifier = Modifier.fillMaxSize()) {
127129
// 文件信息头部
128130
FileHeader(content = content)
129-
131+
130132
HorizontalDivider(
131133
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
132134
)
133-
135+
134136
// 文件内容
135137
FileContentBody(content = content)
136138
}
@@ -141,7 +143,7 @@ private fun FileContentDisplay(content: FileContent) {
141143
*/
142144
@Composable
143145
private fun FileHeader(content: FileContent) {
144-
val clipboardManager = LocalClipboardManager.current
146+
val clipboardManager = LocalClipboard.current
145147
val scope = rememberCoroutineScope()
146148
var showCopyFeedback by remember { mutableStateOf(false) }
147149

@@ -180,11 +182,12 @@ private fun FileHeader(content: FileContent) {
180182
color = MaterialTheme.colorScheme.primary
181183
)
182184
}
183-
185+
184186
IconButton(
185187
onClick = {
186188
scope.launch {
187-
clipboardManager.setText(AnnotatedString(content.content))
189+
clipboardManager.nativeClipboard
190+
clipboardManager.setClipEntry(ClipEntry.withPlainText(content.content))
188191
showCopyFeedback = true
189192
kotlinx.coroutines.delay(2000)
190193
showCopyFeedback = false
@@ -224,15 +227,15 @@ private fun FileContentBody(content: FileContent) {
224227
) {
225228
// 行号列
226229
LineNumbers(content = content.content)
227-
230+
228231
// 分隔线
229232
VerticalDivider(
230233
modifier = Modifier
231234
.fillMaxHeight()
232235
.padding(horizontal = 8.dp),
233236
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)
234237
)
235-
238+
236239
// 代码内容
237240
CodeContent(
238241
content = content.content,
@@ -247,19 +250,25 @@ private fun FileContentBody(content: FileContent) {
247250
*/
248251
@Composable
249252
private fun LineNumbers(content: String) {
253+
val jetBrainsMonoFontFamily = FontFamily(
254+
Font(Res.font.JetBrainsMono_Medium, FontWeight.Medium)
255+
)
256+
250257
val lines = content.split('\n')
251258
val maxLineNumber = lines.size
252259
val lineNumberWidth = maxLineNumber.toString().length
253-
260+
254261
Column(
255262
modifier = Modifier.padding(end = 8.dp)
256263
) {
257264
lines.forEachIndexed { index, _ ->
258265
Text(
259266
text = (index + 1).toString().padStart(lineNumberWidth),
267+
// fontFamily = jetBrainsMonoFontFamily,
260268
style = MaterialTheme.typography.bodySmall.copy(
261-
fontFamily = FontFamily.Monospace,
262-
fontSize = 12.sp
269+
fontFamily = jetBrainsMonoFontFamily,
270+
fontSize = 16.sp,
271+
lineHeight = 16.sp
263272
),
264273
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
265274
modifier = Modifier.padding(vertical = 1.dp)
@@ -273,19 +282,23 @@ private fun LineNumbers(content: String) {
273282
*/
274283
@Composable
275284
private fun CodeContent(content: String, mimeType: String) {
285+
val jetBrainsMonoFontFamily = FontFamily(
286+
Font(Res.font.JetBrainsMono_Medium, FontWeight.Medium)
287+
)
288+
276289
val highlightedText = remember(content, mimeType) {
277290
highlightCode(content, mimeType)
278291
}
279-
292+
280293
SelectionContainer {
281294
Text(
282295
text = highlightedText,
283296
style = MaterialTheme.typography.bodySmall.copy(
284-
fontFamily = FontFamily.Monospace,
285-
fontSize = 12.sp,
297+
fontFamily = jetBrainsMonoFontFamily,
298+
fontSize = 16.sp,
286299
lineHeight = 16.sp
287300
),
288-
modifier = Modifier.padding(vertical = 1.dp)
301+
modifier = Modifier.padding(vertical = 1.dp),
289302
)
290303
}
291304
}
@@ -296,7 +309,7 @@ private fun CodeContent(content: String, mimeType: String) {
296309
private fun highlightCode(content: String, mimeType: String): AnnotatedString {
297310
return buildAnnotatedString {
298311
append(content)
299-
312+
300313
// 根据 MIME 类型应用不同的高亮规则
301314
when (mimeType) {
302315
"text/x-kotlin" -> applyKotlinHighlight(this, content)
@@ -313,12 +326,12 @@ private fun highlightCode(content: String, mimeType: String): AnnotatedString {
313326
*/
314327
private fun applyKotlinHighlight(builder: AnnotatedString.Builder, content: String) {
315328
val keywords = setOf(
316-
"class", "interface", "fun", "val", "var", "if", "else", "when", "for",
329+
"class", "interface", "fun", "val", "var", "if", "else", "when", "for",
317330
"while", "do", "try", "catch", "finally", "return", "break", "continue",
318331
"object", "companion", "data", "sealed", "enum", "annotation", "suspend",
319332
"import", "package", "private", "public", "protected", "internal"
320333
)
321-
334+
322335
highlightKeywords(builder, content, keywords, Color(0xFF0000FF)) // 蓝色关键字
323336
highlightStrings(builder, content, Color(0xFF008000)) // 绿色字符串
324337
highlightComments(builder, content, Color(0xFF808080)) // 灰色注释
@@ -335,7 +348,7 @@ private fun applyJavaHighlight(builder: AnnotatedString.Builder, content: String
335348
"catch", "finally", "throw", "throws", "return", "break", "continue",
336349
"import", "package", "extends", "implements", "super", "this", "new"
337350
)
338-
351+
339352
highlightKeywords(builder, content, keywords, Color(0xFF0000FF))
340353
highlightStrings(builder, content, Color(0xFF008000))
341354
highlightComments(builder, content, Color(0xFF808080))
@@ -369,7 +382,7 @@ private fun applyJsonHighlight(builder: AnnotatedString.Builder, content: String
369382
end = match.range.last + 1
370383
)
371384
}
372-
385+
373386
// 数字
374387
val numberRegex = Regex("\\b\\d+(\\.\\d+)?\\b")
375388
numberRegex.findAll(content).forEach { match ->
@@ -394,8 +407,8 @@ private fun applyGenericHighlight(builder: AnnotatedString.Builder, content: Str
394407
* 关键字高亮
395408
*/
396409
private fun highlightKeywords(
397-
builder: AnnotatedString.Builder,
398-
content: String,
410+
builder: AnnotatedString.Builder,
411+
content: String,
399412
keywords: Set<String>,
400413
color: Color
401414
) {
@@ -437,7 +450,7 @@ private fun highlightComments(builder: AnnotatedString.Builder, content: String,
437450
end = match.range.last + 1
438451
)
439452
}
440-
453+
441454
val blockCommentRegex = Regex("/\\*.*?\\*/", RegexOption.DOT_MATCHES_ALL)
442455
blockCommentRegex.findAll(content).forEach { match ->
443456
builder.addStyle(

composeApp/src/wasmJsMain/kotlin/love/forte/simbot/codegen/gen/view/preview/FileTreeComponents.kt

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,53 @@ import androidx.compose.ui.text.font.FontWeight
2828
import androidx.compose.ui.unit.dp
2929
import androidx.compose.ui.unit.sp
3030

31+
/**
32+
* 计算应该自动展开的路径
33+
* 递归展开只有唯一子目录的目录
34+
*/
35+
private fun calculateAutoExpandPaths(nodes: List<ZipFileNode>): Set<String> {
36+
val autoExpandPaths = mutableSetOf<String>()
37+
38+
fun shouldAutoExpand(node: ZipFileNode): Boolean {
39+
if (!node.isDirectory) return false
40+
41+
// 统计子目录数量
42+
val childDirectories = node.children.filter { it.isDirectory }
43+
44+
// 如果只有一个子目录,则应该自动展开
45+
return childDirectories.size == 1
46+
}
47+
48+
fun collectAutoExpandPaths(nodeList: List<ZipFileNode>) {
49+
for (node in nodeList) {
50+
if (node.isDirectory) {
51+
// 如果是顶层目录,总是展开
52+
if (node.isTopLevel) {
53+
autoExpandPaths.add(node.path)
54+
}
55+
56+
// 如果应该自动展开,添加到集合中
57+
if (shouldAutoExpand(node)) {
58+
autoExpandPaths.add(node.path)
59+
60+
// 对唯一的子目录递归处理
61+
val childDir = node.children.first { it.isDirectory }
62+
collectAutoExpandPaths(listOf(childDir))
63+
}
64+
65+
// 递归处理所有子节点
66+
collectAutoExpandPaths(node.children)
67+
}
68+
}
69+
}
70+
71+
collectAutoExpandPaths(nodes)
72+
return autoExpandPaths
73+
}
74+
3175
/**
3276
* 文件树展示组件
33-
* 支持展开/折叠,默认展开第一层
77+
* 支持展开/折叠,默认展开第一层,自动展开只有唯一子目录的目录
3478
*/
3579
@Composable
3680
fun FileTreeView(
@@ -39,16 +83,15 @@ fun FileTreeView(
3983
onFileSelect: (ZipFileNode) -> Unit,
4084
modifier: Modifier = Modifier
4185
) {
42-
// 展开状态管理,默认展开第一层
43-
val expandedPaths = remember {
44-
mutableStateOf(
45-
nodes.filter { it.isDirectory && it.isTopLevel }.map { it.path }.toSet()
46-
)
86+
// 展开状态管理,默认展开第一层和自动展开单子目录
87+
val expandedPaths = remember(nodes) {
88+
mutableStateOf(calculateAutoExpandPaths(nodes))
4789
}
4890

4991
LazyColumn(
5092
modifier = modifier.fillMaxWidth(),
51-
verticalArrangement = Arrangement.spacedBy(2.dp)
93+
verticalArrangement = Arrangement.spacedBy(4.dp),
94+
contentPadding = PaddingValues(vertical = 8.dp)
5295
) {
5396
items(nodes) { node ->
5497
FileTreeNode(
@@ -90,12 +133,12 @@ private fun FileTreeNode(
90133
Row(
91134
modifier = Modifier
92135
.fillMaxWidth()
93-
.clip(RoundedCornerShape(4.dp))
136+
.clip(RoundedCornerShape(8.dp))
94137
.background(
95-
color = if (isSelected) {
96-
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
97-
} else {
98-
MaterialTheme.colorScheme.surface
138+
color = when {
139+
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
140+
node.isDirectory -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
141+
else -> MaterialTheme.colorScheme.surface
99142
}
100143
)
101144
.clickable(
@@ -105,10 +148,10 @@ private fun FileTreeNode(
105148
onNodeClick(node)
106149
}
107150
.padding(
108-
start = (8 + level * 16).dp,
109-
top = 4.dp,
110-
bottom = 4.dp,
111-
end = 8.dp
151+
start = (12 + level * 20).dp,
152+
top = 8.dp,
153+
bottom = 8.dp,
154+
end = 12.dp
112155
),
113156
verticalAlignment = Alignment.CenterVertically
114157
) {

composeApp/src/wasmJsMain/kotlin/love/forte/simbot/codegen/gen/view/preview/ZipPreviewModels.kt

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ package love.forte.simbot.codegen.gen.view.preview
22

33
import jszip.JSZip
44
import jszip.JSZipObject
5-
import jszip.JSZipGeneratorOptions
6-
import jszip.OutputType
7-
import js.objects.unsafeJso
8-
import js.promise.await
95
import jszip.text
106

117
/**
@@ -225,19 +221,6 @@ class FileContentLoader {
225221
if (node.isDirectory || node.zipObject == null) return null
226222

227223
return try {
228-
// TODO: 调试正确的 JSZipObject API 用法
229-
// 暂时使用占位符,显示文件信息而不是实际内容
230-
val placeholder = """
231-
文件路径: ${node.path}
232-
文件名称: ${node.name}
233-
文件扩展名: ${node.extension}
234-
235-
注意: 文件内容读取功能正在开发中
236-
这是一个占位符内容,用于测试预览功能的基本结构。
237-
238-
实际的文件内容将在 JSZipObject API 调试完成后显示。
239-
""".trimIndent()
240-
241224
val content = node.zipObject.text()
242225
val mimeType = FileContent.inferMimeType(node.name)
243226
val isBinary = false

0 commit comments

Comments
 (0)