Skip to content

Commit 40fb742

Browse files
authored
Merge pull request #1247 from ZalithLauncher/feat/homepage_events
feat(主页): 支持一些新事件、随即文本块
2 parents ab6a828 + 3ad1f66 commit 40fb742

5 files changed

Lines changed: 236 additions & 26 deletions

File tree

ZalithLauncher/src/main/assets/home_page/doc_page_en.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ In addition to standard Markdown, you can also use the following extension compo
1515

1616
---
1717

18+
### Random Text Block
19+
Place multiple text segments within a block. When the homepage loads, one of the segments will be randomly displayed.
20+
21+
**Syntax**
22+
...random-start
23+
This is a piece of text, weight defaults to 1.0
24+
weight(2.0): This is the second piece of text, weight is set to 2.0
25+
This is the third piece of text
26+
...random-end
27+
28+
**Parameter description:**
29+
- Unlike other components, this component uses inline parameters, embedded directly at the beginning of the actual value line.
30+
- `weight`:
31+
- Specifies the weight value for this piece of text. Supports integers and decimals. Optional.
32+
- The homepage will randomly select a piece of text according to the weights.
33+
34+
> This component is only supported within standard Markdown.
35+
> This component will NOT take effect inside standard Markdown containers or other extension components.
36+
37+
---
38+
1839
### Card Component
1940
Used to wrap content inside a container with a background and rounded corners.
2041

@@ -86,9 +107,11 @@ Creates a clickable button.
86107
- `event`: The event to trigger, optional. The value must be wrapped in double quotes, and event data is wrapped in curly braces.
87108
- `url{...}`: Opens a link in the browser.
88109
- `check_update`: Triggers the launcher to check for updates.
89-
- `launch_game`: Launches the currently selected version.
110+
- `launch_game{server=...}`: Launches the currently selected version.
111+
- Parameter `server`: Specifies the server to quick-join after launch, optional.
90112
- `copy{...}`: Copies the specified content.
91-
- For more events, please refer to the launcher's actual supported list.
113+
- `refresh_page`: Refreshes the current homepage.
114+
- `share_game_log`: Shares the log of the current game version.
92115
- `width`: The width of the button, optional.
93116
- You can use a percentage width, calculated based on the actual width of the homepage and the containing layout component: `50%` (only integer percentages supported).
94117
- You can use DP units to set a more specific width: `200dp` (supports integers and decimals).

ZalithLauncher/src/main/assets/home_page/doc_page_zh.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@
1515

1616
---
1717

18+
### 随机文本块
19+
在一个区块内填入多段文本,主页在加载时,将随机显示其中的一段文本
20+
21+
**语法**
22+
...random-start
23+
这是一段文本,权重默认为 1.0
24+
weight(2.0): 这是第二段文本,权重被配置为 2.0
25+
这是第三段文本
26+
...random-end
27+
28+
**参数说明:**
29+
- 与其他组件不同,该组件的参数为内嵌参数,直接嵌入到实际值行首
30+
- `weight`:
31+
- 可填写该段文本的具体的权重值,支持整数、小数,可选
32+
- 主页将按权重随机抽取文本
33+
34+
> 该组件仅支持在基础的 Markdown 内使用
35+
> 该组件不会生效于标准 Markdown 容器或其他扩展组件内
36+
37+
---
38+
1839
### 卡片组件
1940
用于将内容包裹在一个有背景和圆角的容器中
2041

@@ -86,9 +107,11 @@ contentPadding=(4, 4, 12, 12)
86107
- `event`: 触发的事件,可选,值需使用双引号包裹,使用花括号包裹事件数据
87108
- `url{...}`: 在浏览器中打开链接
88109
- `check_update`: 触发启动器检查更新
89-
- `launch_game`: 启动当前选中的版本
110+
- `launch_game{server=...}`: 启动当前选中的版本
111+
- 参数 `server`:指定快速启动并加入的服务器,可选
90112
- `copy{...}`: 复制指定内容
91-
- 更多事件请参考启动器的实际支持列表
113+
- `refresh_page`: 刷新当前主页
114+
- `share_game_log`: 分享当前游戏版本的日志
92115
- `width`: 按钮的宽度,可选
93116
- 可使用百分比宽度,会根据主页的实际宽度、所在布局组件的宽度计算:`50%`,仅支持整数百分比
94117
- 可使用 DP 单位来设置更具体的宽度:`200dp`,支持整数、小数

ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/activities/MainActivity.kt

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.movtery.zalithlauncher.context.COPY_LABEL_LINK
4444
import com.movtery.zalithlauncher.coroutine.Task
4545
import com.movtery.zalithlauncher.coroutine.TaskSystem
4646
import com.movtery.zalithlauncher.game.control.ControlManager
47+
import com.movtery.zalithlauncher.game.version.installed.VersionsManager
4748
import com.movtery.zalithlauncher.notification.NotificationManager
4849
import com.movtery.zalithlauncher.path.URL_SUPPORT
4950
import com.movtery.zalithlauncher.setting.AllSettings
@@ -67,6 +68,7 @@ import com.movtery.zalithlauncher.utils.festival.getTodayFestivals
6768
import com.movtery.zalithlauncher.utils.file.shareFile
6869
import com.movtery.zalithlauncher.utils.isChinese
6970
import com.movtery.zalithlauncher.utils.logging.Logger.lInfo
71+
import com.movtery.zalithlauncher.utils.logging.Logger.lWarning
7072
import com.movtery.zalithlauncher.utils.network.openLink
7173
import com.movtery.zalithlauncher.utils.network.openLinkInternal
7274
import com.movtery.zalithlauncher.utils.string.getMessageOrToString
@@ -468,29 +470,82 @@ class MainActivity : BaseAppCompatActivity() {
468470
key: String,
469471
data: String?
470472
) {
471-
when (key) {
472-
"url" -> {
473-
if (data != null) {
474-
withContext(Dispatchers.Main) {
475-
this@MainActivity.openLink(data)
473+
runCatching {
474+
when (key) {
475+
//浏览器内打开指定链接
476+
"url" -> {
477+
data?.let { url ->
478+
val trimmed = url.trim()
479+
//防止 file://、intent:// 等危险 scheme
480+
if (trimmed.startsWith("http://", ignoreCase = true) ||
481+
trimmed.startsWith("https://", ignoreCase = true)
482+
) {
483+
withContext(Dispatchers.Main) {
484+
this@MainActivity.openLink(trimmed)
485+
}
486+
} else {
487+
lWarning("Blocked unsafe URL from homepage event: $trimmed")
488+
}
476489
}
477490
}
478-
}
479-
"check_update" -> checkUpdate()
480-
"launch_game" -> launchGameViewModel.tryLaunch()
481-
"copy" -> {
482-
if (data != null) {
483-
withContext(Dispatchers.Main) {
484-
copyText(
485-
null,
486-
data,
487-
this@MainActivity,
488-
showToast = true
489-
)
491+
//检查启动器更新
492+
"check_update" -> checkUpdate()
493+
//启动当前选中的游戏版本
494+
"launch_game" -> {
495+
val serverIp = data?.let { raw ->
496+
runCatching {
497+
val parms = raw.split("=", limit = 2)
498+
if (parms.size == 2 && parms[0] == "server") {
499+
parms[1].trim()
500+
} else null
501+
}.onFailure { e ->
502+
lWarning("Failed to parse quick join server parameters: $raw", e)
503+
}.getOrNull()
504+
}
505+
if (!serverIp.isNullOrEmpty()) {
506+
//禁止控制字符与换行,防止注入命令行参数或配置文件
507+
if (serverIp.none { it.code < 32 }) {
508+
launchGameViewModel.tryPlayServer(serverIp)
509+
} else {
510+
lWarning("Invalid server address from homepage event: $serverIp")
511+
}
512+
} else {
513+
launchGameViewModel.tryLaunch()
514+
}
515+
}
516+
//复制指定文本
517+
"copy" -> {
518+
data?.let { text ->
519+
val trimmed = text.trim()
520+
withContext(Dispatchers.Main) {
521+
copyText(
522+
null,
523+
trimmed.take(10_000), //限制复制内容长度
524+
this@MainActivity,
525+
showToast = true
526+
)
527+
}
490528
}
491529
}
530+
//刷新主页
531+
"refresh_page" -> homePageViewModel.reloadPage(true)
532+
//分享游戏日志
533+
"share_game_log" -> {
534+
VersionsManager.currentVersion.value?.let { version ->
535+
VersionsManager.getLatestLog(version).takeIf { it.exists() }
536+
}?.let { logFile ->
537+
withContext(Dispatchers.Main) {
538+
logsUploadViewModel.check(logFile)
539+
logShareViewModel.openMenu(logFile)
540+
}
541+
}
542+
}
543+
else -> {
544+
lWarning("Unknown homepage event: key=$key, data=$data")
545+
}
492546
}
493-
"refresh_page" -> homePageViewModel.reloadPage(true)
547+
}.onFailure { e ->
548+
lWarning("Failed to handle homepage event: key=$key, data=$data", e)
494549
}
495550
}
496551

ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/custom_home/CustomHome.kt

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.halilibo.richtext.ui.RichTextStyle
4040
import com.movtery.zalithlauncher.ui.components.MarkdownView
4141
import com.movtery.zalithlauncher.ui.components.defaultRichTextStyle
4242
import com.movtery.zalithlauncher.ui.theme.itemColor
43+
import kotlin.random.Random
4344

4445
fun LazyListScope.customHomePage(
4546
blocks: List<MarkdownBlock>,
@@ -399,17 +400,19 @@ private fun parseMarkdownBlocksInternal(
399400
if (match == null) {
400401
//没有更多匹配,添加剩余内容
401402
val remaining = cleared.substring(lastIndex).trim('\n')
402-
if (remaining.isNotEmpty()) {
403-
blocks.add(MarkdownBlock.Normal(astNode = parseMarkdown(remaining)))
403+
val processedRemaining = processRandomBlocksInText(remaining)
404+
if (processedRemaining.isNotEmpty()) {
405+
blocks.add(MarkdownBlock.Normal(astNode = parseMarkdown(processedRemaining)))
404406
}
405407
break
406408
}
407409

408410
//处理匹配项之前的普通文本
409411
if (match.range.first > lastIndex) {
410412
val text = cleared.substring(lastIndex, match.range.first).trim('\n')
411-
if (text.isNotEmpty()) {
412-
blocks.add(MarkdownBlock.Normal(astNode = parseMarkdown(text)))
413+
val processedText = processRandomBlocksInText(text)
414+
if (processedText.isNotEmpty()) {
415+
blocks.add(MarkdownBlock.Normal(astNode = parseMarkdown(processedText)))
413416
}
414417
}
415418

@@ -793,4 +796,101 @@ private fun parseWeight(params: String): Pair<Float, Boolean>? {
793796
val weight = match.groupValues[1].toFloatOrNull() ?: return null
794797
val fill = match.groupValues[2] != "noFill"
795798
return Pair(weight, fill)
799+
}
800+
801+
private const val randomStartTag = "...random-start"
802+
private const val randomEndTag = "...random-end"
803+
private val randomOptionRegex = Regex("""^weight\((\d+(?:\.\d+)?)\)\s*:\s*(.*)$""")
804+
805+
private data class RandomOption(
806+
val text: String,
807+
val weight: Double
808+
)
809+
810+
/**
811+
* 处理文本中的随机文本块,自动将其替换为随机选中的文本
812+
* 语法:
813+
* ```
814+
* ...random-start
815+
* weight(N): 文本内容
816+
* 默认权重的文本内容
817+
* ...random-end
818+
* ```
819+
* - 以 `weight(N): ` 开头可指定权重,否则权重默认为 1
820+
* - 随机抽取后的结果中不包含换行符
821+
*/
822+
private fun processRandomBlocksInText(text: String): String {
823+
if (!text.contains(randomStartTag)) return text
824+
825+
val lines = text.lines()
826+
val result = mutableListOf<String>()
827+
var inRandomBlock = false
828+
var randomStartLine: String? = null
829+
val randomLines = mutableListOf<String>()
830+
831+
for (line in lines) {
832+
val trimmed = line.trimStart()
833+
834+
if (inRandomBlock) {
835+
if (trimmed.startsWith(randomEndTag)) {
836+
val selected = pickRandomText(randomLines)
837+
result.add(selected)
838+
randomLines.clear()
839+
inRandomBlock = false
840+
randomStartLine = null
841+
} else {
842+
randomLines.add(line)
843+
}
844+
} else {
845+
if (trimmed.startsWith(randomStartTag)) {
846+
inRandomBlock = true
847+
randomStartLine = line
848+
} else {
849+
result.add(line)
850+
}
851+
}
852+
}
853+
854+
//如果文本结束但随机块未闭合,则忽略此随机文本块
855+
if (inRandomBlock) {
856+
randomStartLine?.let { result.add(it) }
857+
result.addAll(randomLines)
858+
}
859+
860+
return result.joinToString("\n")
861+
}
862+
863+
/**
864+
* 从随机文本候选项中根据权重抽取一个文本
865+
*/
866+
private fun pickRandomText(lines: List<String>): String {
867+
val options = lines.mapNotNull { line ->
868+
val trimmed = line.trim()
869+
if (trimmed.isEmpty()) return@mapNotNull null
870+
871+
//读取权重值
872+
val match = randomOptionRegex.find(trimmed)
873+
if (match != null) {
874+
val weight = match.groupValues[1].toDoubleOrNull() ?: 1.0
875+
val text = match.groupValues[2]
876+
RandomOption(text, weight)
877+
} else {
878+
//未配置权重时,默认为 1
879+
RandomOption(trimmed, 1.0)
880+
}
881+
}
882+
883+
if (options.isEmpty()) return ""
884+
885+
val totalWeight = options.sumOf { it.weight }
886+
var randomValue = Random.nextDouble() * totalWeight
887+
888+
for (option in options) {
889+
randomValue -= option.weight
890+
if (randomValue <= 0) {
891+
return option.text
892+
}
893+
}
894+
895+
return options.last().text
796896
}

ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/LaunchGameViewModel.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ class LaunchGameViewModel : ViewModel() {
6161
}
6262
}
6363

64+
/**
65+
* 尝试启动游戏快速游玩服务器
66+
* @param address 服务器地址
67+
*/
68+
fun tryPlayServer(address: String) {
69+
val version = VersionsManager.currentVersion.value ?: return
70+
quickPlayServer(version, address)
71+
}
72+
6473
/**
6574
* 通过服务器列表快速游玩服务器
6675
* @param address 服务器地址

0 commit comments

Comments
 (0)