├── .github
├── ISSUE_TEMPLATE
│ └── ----.md
└── workflows
│ └── Release.yml
├── .gitignore
├── README.md
├── app
├── build.gradle.kts
└── src
│ ├── main
│ ├── kotlin
│ │ ├── EntryPoint.kt
│ │ ├── Hack.kt
│ │ ├── MiraiComposeWindow.kt
│ │ ├── console
│ │ │ ├── LoginSolver.kt
│ │ │ ├── MiraiComposeImplementation.kt
│ │ │ ├── ViewModel.kt
│ │ │ └── impl
│ │ │ │ ├── ComposeLog.kt
│ │ │ │ ├── MiraiComposeDataStore.kt
│ │ │ │ ├── MiraiComposeImpl.kt
│ │ │ │ ├── ReadablePluginStorage.kt
│ │ │ │ └── ViewModelStoreImpl.kt
│ │ ├── model
│ │ │ └── LoginCredential.kt
│ │ ├── resource
│ │ │ └── R.kt
│ │ ├── ui
│ │ │ ├── BotItem.kt
│ │ │ ├── HostPage.kt
│ │ │ ├── NavHostBotMenu.kt
│ │ │ ├── RailTab.kt
│ │ │ ├── Utils.kt
│ │ │ ├── about
│ │ │ │ └── About.kt
│ │ │ ├── log
│ │ │ │ └── ConsoleLog.kt
│ │ │ ├── login
│ │ │ │ ├── LoginDialog.kt
│ │ │ │ └── LoginSolverContent.kt
│ │ │ ├── message
│ │ │ │ ├── BotMessage.kt
│ │ │ │ └── Message.kt
│ │ │ ├── plugins
│ │ │ │ ├── Annotation.kt
│ │ │ │ ├── PluginList.kt
│ │ │ │ ├── Plugins.kt
│ │ │ │ └── SinglePlugin.kt
│ │ │ └── setting
│ │ │ │ ├── AutoLogin.kt
│ │ │ │ └── Setting.kt
│ │ └── viewmodel
│ │ │ ├── ConsoleLogViewModel.kt
│ │ │ ├── HostViewModel.kt
│ │ │ └── PluginsViewModel.kt
│ └── resources
│ │ ├── ic_close.xml
│ │ ├── ic_java.xml
│ │ ├── ic_kotlin.xml
│ │ ├── ic_max.xml
│ │ ├── ic_min.xml
│ │ ├── ic_mirai.xml
│ │ └── mirai.png
│ └── test
│ └── kotlin
│ ├── Entrypoint.kt
│ ├── TestPlugin.kt
│ └── console
│ ├── FluentMVITest.kt
│ ├── MiraiComposeImplTest.kt
│ └── ViewModelTest.kt
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── MiraiCompose.kt
├── docs
├── FEATURES.md
└── design.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/ISSUE_TEMPLATE/----.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 异常报告
3 | about: 报告一个异常
4 | title: 异常报告
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 | **简要说明出现的异常**
11 | 预期出现的表现和实际上的表现
12 |
13 | **复现步骤(触发条件)**
14 | 1. Go to '...'
15 | 2. Click on '....'
16 | 3. Scroll down to '....'
17 | 4. See error
18 |
19 | **日志截图或内容**
20 | ```
21 | ```
22 |
23 | **环境信息**
24 | - OS: `Windows`
25 | - Version `v1.0.0
--------------------------------------------------------------------------------
/.github/workflows/Release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 | paths-ignore:
8 | - README.md
9 | - icons/*
10 | - LICENSE
11 |
12 | jobs:
13 | job_upload_release:
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | os: [ windows-latest, ubuntu-latest, macOS-latest ]
18 | include:
19 | - os: windows-latest
20 | format: msi
21 |
22 | - os: ubuntu-latest
23 | format: deb
24 |
25 | - os: macOS-latest
26 | format: dmg
27 | steps:
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 |
31 | - name: Set up JDK 17
32 | uses: actions/setup-java@v1
33 | with:
34 | java-version: 17
35 |
36 | - name: Init gradle project
37 | run: ./gradlew clean
38 |
39 | - name: Build Package
40 | run: ./gradlew package
41 |
42 | - name: Upload res
43 | uses: actions/upload-artifact@v2
44 | with:
45 | name: mirai-compose-${{ matrix.os }}
46 | path: app/build/compose/binaries/main/${{ matrix.format }}/*
47 |
48 | job_dowland_release:
49 | runs-on: ubuntu-latest
50 | needs: job_upload_release
51 | steps:
52 | - name: Checkout code
53 | uses: actions/checkout@v2
54 |
55 | - name: Download
56 | id: download
57 | uses: actions/download-artifact@v2
58 | with:
59 | path: ./download
60 |
61 | - name: Release
62 | uses: softprops/action-gh-release@v1
63 | with:
64 | files: |
65 | ${{ steps.download.outputs.download-path }}/mirai-compose-macOS-latest/*
66 | ${{ steps.download.outputs.download-path }}/mirai-compose-ubuntu-latest/*
67 | ${{ steps.download.outputs.download-path }}/mirai-compose-windows-latest/*
68 | token: ${{ secrets.GITHUB_TOKEN }}
69 |
70 | - name: Sync to mirai forum
71 | uses: SamKirkland/FTP-Deploy-Action@4.2.0
72 | with:
73 | server: ${{ secrets.ftp_server }}
74 | username: ${{ secrets.ftp_account }}
75 | password: ${{ secrets.ftp_password }}
76 | local-dir: ${{ steps.download.outputs.download-path }}
77 | protocol: ftp
78 | options: "--delete"
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea\
2 | .gradle\
3 | build\
4 | data\
5 | log\
6 | plugins\
7 | run\
8 | config\
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
15 |
16 | # mirai-compose
17 |
18 | [](https://github.com/sonder-joker/mirai-compose/releases)
19 | 
20 | [](https://mirai.mamoe.net)
21 |
22 | ## 这是什么?
23 |
24 | 这是一个基于[compose-jb](https://github.com/jetbrains/compose-jb) ,实现的[mirai-console](https://github.com/mamoe/mirai-console)
25 | 前端。
26 |
27 | ## 他稳定吗?
28 |
29 | 目前还是属于不稳定但能够使用的阶段,欢迎各位来提issue和feature。
30 |
31 | ## 如何下载?
32 |
33 | 可以在[右侧的release页面](https://github.com/sonder-joker/mirai-compose/releases) 下载符合当前操作系统的版本。
34 | 同样拥有国内镜像,在 [mirai-forum论坛的帖子内](https://mirai.mamoe.net/topic/215/mirai-compose-%E8%B7%A8%E5%B9%B3%E5%8F%B0-%E5%9B%BE%E5%BD%A2%E5%8C%96-%E6%98%93%E5%AE%89%E8%A3%85%E7%9A%84mirai-console%E5%AE%A2%E6%88%B7%E7%AB%AF)
35 | 可以下载
36 |
37 | ## 如何使用?
38 |
39 | 直接安装即可,功能详见[文档](docs/FEATURES.md)。
40 |
41 | ## 局限
42 |
43 | 由于compose-jb并不提供32位环境,使用32位的MiraiNative会造成意想不到的结果。
44 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.compose
2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
3 | import org.jetbrains.compose.ExperimentalComposeLibrary
4 |
5 | plugins {
6 | kotlin("jvm")
7 | kotlin("plugin.serialization")
8 | id("org.jetbrains.compose")
9 | id("com.github.gmazzo.buildconfig")
10 | }
11 |
12 | dependencies {
13 | api(libs.mirai.core)
14 | api(libs.mirai.console)
15 |
16 | implementation(libs.serialization.yaml)
17 |
18 | implementation(compose.desktop.currentOs)
19 | implementation(compose.materialIconsExtended)
20 | implementation(compose.uiTooling)
21 |
22 | @OptIn(ExperimentalComposeLibrary::class)
23 | implementation(compose.desktop.components.splitPane)
24 |
25 | implementation(kotlin("stdlib"))
26 | implementation(kotlin("stdlib-common"))
27 | implementation(kotlin("stdlib-jdk7"))
28 | implementation(kotlin("stdlib-jdk8"))
29 | implementation(kotlin("reflect"))
30 |
31 | testImplementation(libs.junit4)
32 | testImplementation(kotlin("test-junit"))
33 | testImplementation(kotlin("test"))
34 |
35 | @OptIn(ExperimentalComposeLibrary::class)
36 | testImplementation(compose.uiTestJUnit4)
37 | }
38 |
39 | buildConfig {
40 | buildConfigField("String", "projectName", "\"${MiraiCompose.name}\"")
41 | buildConfigField("String", "projectGroup", "\"${MiraiCompose.group}\"")
42 | buildConfigField("String", "projectVersion", "\"${MiraiCompose.version}\"")
43 | }
44 |
45 | compose.desktop {
46 | application {
47 | mainClass = MiraiCompose.mainClass
48 | jvmArgs += "-Dmirai.slider.captcha.supported"
49 | nativeDistributions {
50 | includeAllModules = true
51 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
52 |
53 | packageName = MiraiCompose.name
54 | packageVersion = MiraiCompose.version
55 | vendor = MiraiCompose.group
56 |
57 | macOS {
58 | bundleID = MiraiCompose.group
59 | }
60 |
61 | linux {
62 | }
63 |
64 | windows {
65 | dirChooser = true
66 | upgradeUuid = MiraiCompose.windowsUUID
67 | }
68 | }
69 | }
70 | }
71 |
72 |
73 |
74 | kotlin {
75 | sourceSets.all {
76 | languageSettings.optIn("net.mamoe.mirai.console.util.ConsoleExperimentalApi")
77 | languageSettings.optIn("net.mamoe.mirai.utils.MiraiExperimentalApi")
78 | languageSettings.optIn("net.mamoe.mirai.console.ConsoleFrontEndImplementation")
79 | languageSettings.optIn("androidx.compose.foundation.ExperimentalFoundationApi")
80 | }
81 | }
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/EntryPoint.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.text.selection.SelectionContainer
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.ui.res.loadImageBitmap
10 | import androidx.compose.ui.window.application
11 | import com.youngerhousea.mirai.compose.console.Solver
12 | import com.youngerhousea.mirai.compose.console.impl.MiraiCompose
13 | import com.youngerhousea.mirai.compose.resource.color
14 | import com.youngerhousea.mirai.compose.ui.HostPage
15 | import com.youngerhousea.mirai.compose.ui.login.LoginSolverContent
16 | import kotlinx.coroutines.asCoroutineDispatcher
17 | import kotlinx.coroutines.cancel
18 | import kotlinx.coroutines.launch
19 | import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
20 | import java.io.ByteArrayInputStream
21 | import java.util.concurrent.Executors
22 |
23 | private val ConsoleThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
24 |
25 | fun main() {
26 | startApplication {}
27 | }
28 |
29 | fun startApplication(extraAction: () -> Unit) = application {
30 | val scope = rememberCoroutineScope()
31 | MaterialTheme(colors = color) {
32 | MiraiComposeWindow(onLoaded = {
33 | scope.launch(ConsoleThread) {
34 | MiraiCompose.start()
35 | extraAction()
36 | }
37 | }, onCloseRequest = {
38 | MiraiCompose.cancel()
39 | exitApplication()
40 | }) {
41 |
42 | val state by MiraiCompose.solverState
43 | state?.apply {
44 | MiraiComposeDialog({
45 | MiraiCompose.dispatch(Solver.Action.ExitProcessSolver)
46 | }) {
47 | LoginSolverContent(title = "Bot:${bot.id}",
48 | tip = title,
49 | load = {
50 | when (kind) {
51 | Solver.Kind.Pic -> Image(
52 | loadImageBitmap(ByteArrayInputStream(data.toByteArray())),
53 | null
54 | )
55 | Solver.Kind.Slider, Solver.Kind.Unsafe -> {
56 | SelectionContainer {
57 | Text(data)
58 | }
59 | }
60 | }
61 | },
62 | onFinish = { MiraiCompose.dispatch(Solver.Action.CompleteVerify(it)) },
63 | refresh = { MiraiCompose.dispatch(Solver.Action.RefreshData) },
64 | exit = { MiraiCompose.dispatch(Solver.Action.ExitProcessSolver) })
65 | }
66 | }
67 |
68 | HostPage()
69 | }
70 | }
71 |
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/Hack.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
2 |
3 | package com.youngerhousea.mirai.compose
4 |
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.rememberUpdatedState
9 | import androidx.compose.ui.awt.ComposeWindow
10 | import androidx.compose.ui.graphics.painter.Painter
11 | import androidx.compose.ui.input.key.KeyEvent
12 | import androidx.compose.ui.unit.DpSize
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.util.setIcon
15 | import androidx.compose.ui.util.setPositionSafely
16 | import androidx.compose.ui.util.setSizeSafely
17 | import androidx.compose.ui.util.setUndecoratedSafely
18 | import androidx.compose.ui.window.*
19 | import java.awt.event.ComponentAdapter
20 | import java.awt.event.ComponentEvent
21 | import java.awt.event.WindowAdapter
22 | import java.awt.event.WindowEvent
23 | import javax.swing.JFrame
24 |
25 | @Composable
26 | fun Window(
27 | onLoaded:() -> Unit,
28 | onCloseRequest: () -> Unit,
29 | state: WindowState = rememberWindowState(),
30 | visible: Boolean = true,
31 | title: String = "Untitled",
32 | icon: Painter? = null,
33 | undecorated: Boolean = false,
34 | transparent: Boolean = false,
35 | resizable: Boolean = true,
36 | enabled: Boolean = true,
37 | focusable: Boolean = true,
38 | alwaysOnTop: Boolean = false,
39 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
40 | onKeyEvent: (KeyEvent) -> Boolean = { false },
41 | content: @Composable FrameWindowScope.() -> Unit
42 | ) {
43 | val currentState by rememberUpdatedState(state)
44 | val currentTitle by rememberUpdatedState(title)
45 | val currentIcon by rememberUpdatedState(icon)
46 | val currentUndecorated by rememberUpdatedState(undecorated)
47 | val currentTransparent by rememberUpdatedState(transparent)
48 | val currentResizable by rememberUpdatedState(resizable)
49 | val currentEnabled by rememberUpdatedState(enabled)
50 | val currentFocusable by rememberUpdatedState(focusable)
51 | val currentAlwaysOnTop by rememberUpdatedState(alwaysOnTop)
52 | val currentOnCloseRequest by rememberUpdatedState(onCloseRequest)
53 | val currentOnLoaded by rememberUpdatedState(onLoaded)
54 |
55 | val updater = remember(::ComponentUpdater)
56 |
57 | // the state applied to the window. exist to avoid races between WindowState changes and the state stored inside the native window
58 | val appliedState = remember {
59 | object {
60 | var size: DpSize? = null
61 | var position: WindowPosition? = null
62 | var placement: WindowPlacement? = null
63 | var isMinimized: Boolean? = null
64 | }
65 | }
66 |
67 | Window(
68 | visible = visible,
69 | onPreviewKeyEvent = onPreviewKeyEvent,
70 | onKeyEvent = onKeyEvent,
71 | create = {
72 | ComposeWindow().apply {
73 | // close state is controlled by WindowState.isOpen
74 | defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE
75 | addWindowListener(object : WindowAdapter() {
76 | override fun windowOpened(e: WindowEvent?) {
77 | currentOnLoaded()
78 | }
79 |
80 | override fun windowClosing(e: WindowEvent) {
81 | currentOnCloseRequest()
82 | }
83 | })
84 | addWindowStateListener {
85 | currentState.placement = placement
86 | currentState.isMinimized = isMinimized
87 | appliedState.placement = currentState.placement
88 | appliedState.isMinimized = currentState.isMinimized
89 | }
90 | addComponentListener(object : ComponentAdapter() {
91 | override fun componentResized(e: ComponentEvent) {
92 | // we check placement here and in windowStateChanged,
93 | // because fullscreen changing doesn't
94 | // fire windowStateChanged, only componentResized
95 | currentState.placement = placement
96 | currentState.size = DpSize(width.dp, height.dp)
97 | appliedState.placement = currentState.placement
98 | appliedState.size = currentState.size
99 | }
100 |
101 | override fun componentMoved(e: ComponentEvent) {
102 | currentState.position = WindowPosition(x.dp, y.dp)
103 | appliedState.position = currentState.position
104 | }
105 | })
106 | }
107 | },
108 | dispose = ComposeWindow::dispose,
109 | update = { window ->
110 | updater.update {
111 | set(currentTitle, window::setTitle)
112 | set(currentIcon, window::setIcon)
113 | set(currentUndecorated, window::setUndecoratedSafely)
114 | set(currentTransparent, window::isTransparent::set)
115 | set(currentResizable, window::setResizable)
116 | set(currentEnabled, window::setEnabled)
117 | set(currentFocusable, window::setFocusable)
118 | set(currentAlwaysOnTop, window::setAlwaysOnTop)
119 | }
120 | if (state.size != appliedState.size) {
121 | window.setSizeSafely(state.size)
122 | appliedState.size = state.size
123 | }
124 | if (state.position != appliedState.position) {
125 | window.setPositionSafely(state.position)
126 | appliedState.position = state.position
127 | }
128 | if (state.placement != appliedState.placement) {
129 | window.placement = state.placement
130 | appliedState.placement = state.placement
131 | }
132 | if (state.isMinimized != appliedState.isMinimized) {
133 | window.isMinimized = state.isMinimized
134 | appliedState.isMinimized = state.isMinimized
135 | }
136 | },
137 | content = content
138 | )
139 | }
140 |
141 | internal class ComponentUpdater {
142 | private var updatedValues = mutableListOf()
143 |
144 | fun update(body: UpdateScope.() -> Unit) {
145 | UpdateScope().body()
146 | }
147 |
148 | inner class UpdateScope {
149 | private var index = 0
150 |
151 | /**
152 | * Compare [value] with the old one and if it is changed - store a new value and call
153 | * [update]
154 | */
155 | fun set(value: T, update: (T) -> Unit) {
156 | if (index < updatedValues.size) {
157 | if (updatedValues[index] != value) {
158 | update(value)
159 | updatedValues[index] = value
160 | }
161 | } else {
162 | check(index == updatedValues.size)
163 | update(value)
164 | updatedValues.add(value)
165 | }
166 |
167 | index++
168 | }
169 | }
170 | }
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/MiraiComposeWindow.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose
2 |
3 | import androidx.compose.desktop.ui.tooling.preview.Preview
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.combinedClickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.window.WindowDraggableArea
9 | import androidx.compose.material.*
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.CloseFullscreen
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.painter.BitmapPainter
17 | import androidx.compose.ui.graphics.painter.Painter
18 | import androidx.compose.ui.input.key.KeyEvent
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.DpSize
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.window.*
23 | import com.youngerhousea.mirai.compose.resource.R
24 | import com.youngerhousea.mirai.compose.ui.closeIcon
25 | import com.youngerhousea.mirai.compose.ui.maxIcon
26 | import com.youngerhousea.mirai.compose.ui.minIcon
27 |
28 | @Composable
29 | fun MiraiComposeWindow(
30 | onLoaded: () -> Unit,
31 | onCloseRequest: () -> Unit,
32 | state: WindowState = rememberWindowState(size = MiraiComposeWindowSize),
33 | content: @Composable WindowScope.() -> Unit,
34 | ) = Window(
35 | onLoaded = onLoaded,
36 | onCloseRequest = onCloseRequest,
37 | state = state,
38 | title = "MiraiCompose",
39 | icon = BitmapPainter(R.Icon.Mirai),
40 | undecorated = true,
41 | resizable = true,
42 | ) {
43 | WindowArea(
44 | minimizeButton = {
45 | Button(
46 | onClick = {
47 | state.isMinimized = true
48 | },
49 | colors = ButtonDefaults.buttonColors(backgroundColor = R.Colors.TopAppBar)
50 | ) {
51 | Icon(minIcon(), null)
52 | }
53 | },
54 | maximizeButton = {
55 | Button(
56 | onClick = state::onMaximizeButtonClick,
57 | colors = ButtonDefaults.buttonColors(backgroundColor = R.Colors.TopAppBar)
58 | ) {
59 | when (state.placement) {
60 | WindowPlacement.Maximized,
61 | WindowPlacement.Fullscreen -> Icon(Icons.Default.CloseFullscreen, null)
62 | WindowPlacement.Floating -> Icon(maxIcon(), null)
63 | }
64 |
65 | }
66 | },
67 | exitButton = {
68 | Button(
69 | onClick = onCloseRequest,
70 | colors = ButtonDefaults.buttonColors(backgroundColor = R.Colors.TopAppBar)
71 | ) {
72 | Icon(closeIcon(12f), null)
73 | }
74 | },
75 | onBarDoubleClick = state::onMaximizeButtonClick,
76 | draggableAreaHeight = WindowDraggableHeight,
77 | ) {
78 | content()
79 | }
80 | }
81 |
82 | fun WindowState.onMaximizeButtonClick() {
83 | placement = when (placement) {
84 | WindowPlacement.Floating -> WindowPlacement.Maximized
85 | WindowPlacement.Maximized -> WindowPlacement.Floating
86 | WindowPlacement.Fullscreen -> WindowPlacement.Floating
87 | }
88 | }
89 |
90 |
91 | @Composable
92 | fun MiraiComposeDialog(
93 | onCloseRequest: () -> Unit,
94 | state: DialogState = rememberDialogState(size = DpSize(642.dp, 376.dp)),
95 | visible: Boolean = true,
96 | title: String = "Dialog",
97 | icon: Painter? = null,
98 | enabled: Boolean = true,
99 | focusable: Boolean = true,
100 | onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
101 | onKeyEvent: ((KeyEvent) -> Boolean) = { false },
102 | content: @Composable DialogWindowScope.() -> Unit
103 | ) = Dialog(
104 | onCloseRequest = onCloseRequest,
105 | state = state,
106 | visible = visible,
107 | title = title,
108 | icon = icon,
109 | undecorated = true,
110 | resizable = true,
111 | enabled = enabled,
112 | focusable = focusable,
113 | onPreviewKeyEvent = onPreviewKeyEvent,
114 | onKeyEvent = onKeyEvent
115 | ) {
116 | WindowArea(
117 | exitButton = {
118 | Button(
119 | onClick = onCloseRequest,
120 | colors = ButtonDefaults.buttonColors(backgroundColor = R.Colors.TopAppBar)
121 | ) {
122 | Icon(closeIcon(50f), null)
123 | }
124 | },
125 | ) {
126 | content()
127 | }
128 | }
129 |
130 |
131 | @Composable
132 | private inline fun WindowScope.WindowArea(
133 | crossinline minimizeButton: @Composable () -> Unit = {},
134 | crossinline maximizeButton: @Composable () -> Unit = {},
135 | crossinline exitButton: @Composable () -> Unit = {},
136 | noinline onBarDoubleClick: () -> Unit = {},
137 | draggableAreaHeight: Dp = DialogDraggableHeight,
138 | crossinline content: @Composable () -> Unit
139 | ) {
140 | Surface(elevation = 1.dp) {
141 | Column(Modifier.fillMaxSize()) {
142 | Row(
143 | modifier = Modifier.background(color = R.Colors.TopAppBar)
144 | .fillMaxWidth()
145 | .height(draggableAreaHeight)
146 | .padding(horizontal = DraggableRightStart),
147 | verticalAlignment = Alignment.CenterVertically
148 | ) {
149 | WindowDraggableArea(
150 | modifier = Modifier.fillMaxSize().weight(1f)
151 | .combinedClickable(
152 | interactionSource = remember { MutableInteractionSource() },
153 | indication = null,
154 | onDoubleClick = onBarDoubleClick,
155 | onClick = {}
156 | )
157 | ) {
158 | }
159 | Row {
160 | minimizeButton()
161 | Spacer(modifier = Modifier.width(DraggableIconSpace))
162 | maximizeButton()
163 | Spacer(modifier = Modifier.width(DraggableIconSpace))
164 | exitButton()
165 | }
166 | }
167 | content()
168 | }
169 | }
170 | }
171 |
172 | @Preview
173 | @Composable
174 | fun WindowsAreaPreview() {
175 | Window({}) {
176 | WindowArea {
177 | Text("Test")
178 | }
179 | }
180 | }
181 |
182 | private val MiraiComposeWindowSize = DpSize(1280.dp, 768.dp)
183 |
184 | private val DraggableRightStart = 10.dp
185 | private val WindowDraggableHeight = 30.dp
186 | private val DialogDraggableHeight = 20.dp
187 | private val DraggableIconSpace = 5.dp
188 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/LoginSolver.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
2 |
3 | package com.youngerhousea.mirai.compose.console
4 |
5 | import androidx.compose.runtime.MutableState
6 | import androidx.compose.runtime.State
7 | import androidx.compose.runtime.mutableStateOf
8 | import kotlinx.coroutines.CompletableDeferred
9 | import net.mamoe.mirai.Bot
10 | import net.mamoe.mirai.console.MiraiConsole
11 | import net.mamoe.mirai.network.CustomLoginFailedException
12 | import net.mamoe.mirai.utils.LoginSolver
13 |
14 | interface Solver {
15 |
16 | enum class Kind {
17 | Pic, // bytearray
18 | Slider, // url
19 | Unsafe // url
20 | }
21 |
22 | data class SolverState(
23 | val bot: Bot,
24 | val data: String,
25 | val title: String,
26 | val kind: Kind,
27 | val verifyData: CompletableDeferred,
28 | val dialogIsOpen: Boolean
29 | )
30 |
31 | val solverState: State
32 |
33 | fun dispatch(action: Action)
34 |
35 | sealed class Action {
36 | class CompleteVerify(val input: String) : Action()
37 | object RefreshData : Action()
38 | object ExitProcessSolver : Action()
39 | }
40 | }
41 |
42 | object MiraiComposeLoginSolver : LoginSolver(), Solver {
43 | override val solverState: MutableState = mutableStateOf(null)
44 |
45 | override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? {
46 | MiraiConsole.mainLogger.info("Start load pic captcha")
47 | solverState.value =
48 | Solver.SolverState(bot, String(data), "处理图片验证码", Solver.Kind.Pic, CompletableDeferred(), true)
49 | try {
50 | return solverState.value!!.verifyData.await()
51 | } catch (e: Exception) {
52 | MiraiConsole.mainLogger.info("Load pic captcha failed")
53 | throw e
54 | } finally {
55 | solverState.value = null
56 | }
57 | }
58 |
59 | override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
60 | MiraiConsole.mainLogger.info("Start load slider captcha")
61 | solverState.value = Solver.SolverState(bot, url, "处理滑动验证码", Solver.Kind.Slider, CompletableDeferred(), true)
62 | try {
63 | return solverState.value!!.verifyData.await()
64 | } catch (e: Exception) {
65 | MiraiConsole.mainLogger.info("Load slider captcha failed")
66 | throw e
67 | } finally {
68 | solverState.value = null
69 | }
70 | }
71 |
72 | override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
73 | MiraiConsole.mainLogger.info("Start load unsafe device")
74 | solverState.value = Solver.SolverState(bot, url, "处理不安全验证码", Solver.Kind.Unsafe, CompletableDeferred(), true)
75 | try {
76 | return solverState.value!!.verifyData.await()
77 | } catch (e: Exception) {
78 | MiraiConsole.mainLogger.info("Load unsafe device failed")
79 | throw e
80 | } finally {
81 | solverState.value = null
82 | }
83 | }
84 |
85 | override fun dispatch(action: Solver.Action) {
86 | solverState.value?.let {
87 | when (action) {
88 | is Solver.Action.CompleteVerify -> it.verifyData.complete(action.input)
89 | Solver.Action.ExitProcessSolver -> it.verifyData.completeExceptionally(UICannotFinish())
90 | Solver.Action.RefreshData -> it.verifyData.complete(null)
91 | }
92 | }
93 | }
94 | }
95 |
96 | class UICannotFinish : CustomLoginFailedException(killBot = true, message = "UI cannot finish login")
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/MiraiComposeImplementation.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console
2 |
3 | import androidx.compose.runtime.State
4 | import com.youngerhousea.mirai.compose.console.impl.Log
5 | import com.youngerhousea.mirai.compose.console.impl.ReadablePluginConfigStorage
6 | import com.youngerhousea.mirai.compose.console.impl.ReadablePluginDataStorage
7 | import net.mamoe.mirai.console.MiraiConsoleImplementation
8 | import net.mamoe.mirai.console.data.PluginConfig
9 | import net.mamoe.mirai.console.data.PluginData
10 | import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
11 | import net.mamoe.mirai.utils.MiraiLogger
12 |
13 | interface MiraiComposeImplementation :
14 | MiraiConsoleImplementation, Solver {
15 | override val configStorageForBuiltIns: ReadablePluginConfigStorage
16 |
17 | override val configStorageForJvmPluginLoader: ReadablePluginConfigStorage
18 |
19 | override val dataStorageForBuiltIns: ReadablePluginDataStorage
20 |
21 | override val dataStorageForJvmPluginLoader: ReadablePluginDataStorage
22 |
23 | val JvmPlugin.data: List
24 |
25 | val JvmPlugin.config: List
26 |
27 | val logStorage: State>
28 |
29 | val composeLogger: MiraiLogger
30 |
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.staticCompositionLocalOf
5 | import com.youngerhousea.mirai.compose.console.impl.ViewModelStoreImpl
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.SupervisorJob
8 | import kotlinx.coroutines.cancel
9 |
10 | interface ViewModel {
11 | fun onDestroy()
12 | }
13 |
14 | abstract class ViewModelScope : ViewModel {
15 | protected val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob())
16 |
17 | override fun onDestroy() {
18 | viewModelScope.cancel()
19 | }
20 | }
21 |
22 | interface ViewModelStore {
23 | fun put(key: Any, viewModel: ViewModel)
24 |
25 | fun get(key: Any): ViewModel?
26 |
27 | fun clean()
28 | }
29 |
30 | inline fun ViewModelStore.get(): T? {
31 | return get(T::class) as T?
32 | }
33 |
34 | inline fun ViewModelStore.getOrCreate(crossinline factory: () -> T): T {
35 | return getOrCreate(T::class, factory)
36 | }
37 |
38 | inline fun ViewModelStore.getOrCreate(key: Any, crossinline factory: () -> T): T {
39 | return get(key) as? T ?: factory().apply { put(key, this) }
40 | }
41 |
42 | internal val viewModelStoreImpl = ViewModelStoreImpl()
43 |
44 | val LocalViewModelStore = staticCompositionLocalOf {
45 | viewModelStoreImpl
46 | }
47 |
48 | @Composable
49 | inline fun viewModel(): T? {
50 | return LocalViewModelStore.current.get()
51 | }
52 |
53 | @Composable
54 | inline fun viewModel(crossinline factory: () -> T): T {
55 | return LocalViewModelStore.current.getOrCreate(factory)
56 | }
57 |
58 | @Composable
59 | inline fun viewModel(key: Any, crossinline factory: () -> T): T {
60 | return LocalViewModelStore.current.getOrCreate(key, factory)
61 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/impl/ComposeLog.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console.impl
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import net.mamoe.mirai.utils.MiraiLoggerPlatformBase
5 | import java.text.DateFormat
6 | import java.text.SimpleDateFormat
7 | import java.util.*
8 |
9 | enum class TextColor(private val format: String) {
10 | RESET("\u001b[0m"),
11 |
12 | WHITE("\u001b[30m"),
13 | RED("\u001b[31m"),
14 | EMERALD_GREEN("\u001b[32m"),
15 | GOLD("\u001b[33m"),
16 | BLUE("\u001b[34m"),
17 | PURPLE("\u001b[35m"),
18 | GREEN("\u001b[36m"),
19 |
20 | GRAY("\u001b[90m"),
21 | LIGHT_RED("\u001b[91m"),
22 | LIGHT_GREEN("\u001b[92m"),
23 | LIGHT_YELLOW("\u001b[93m"),
24 | LIGHT_BLUE("\u001b[94m"),
25 | LIGHT_PURPLE("\u001b[95m"),
26 | LIGHT_CYAN("\u001b[96m");
27 |
28 | override fun toString(): String = format
29 | }
30 |
31 | enum class LogKind(
32 | val simpleName: String,
33 | val color: Color,
34 | val textColor: TextColor
35 | ) {
36 | VERBOSE("V", Color.Blue , TextColor.BLUE),
37 | INFO("I", Color.Green, TextColor.GREEN),
38 | WARNING("W", Color.Red, TextColor.RED),
39 | ERROR("E", Color.Red, TextColor.RED),
40 | DEBUG("D", Color.Cyan, TextColor.LIGHT_CYAN)
41 | }
42 |
43 | data class Log(
44 | val message: String,
45 | val kind: LogKind
46 | )
47 |
48 | private val timeFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE)
49 |
50 | val currentTimeFormatted: String get() = timeFormat.format(Date())
51 |
52 | class MiraiComposeLogger(
53 | override val identity: String?,
54 | val output: (content: String?, exception: Throwable?, priority: LogKind) -> Unit
55 | ) : MiraiLoggerPlatformBase() {
56 |
57 | public override fun verbose0(message: String?, e: Throwable?) =
58 | output(message, e, LogKind.VERBOSE)
59 |
60 | public override fun info0(message: String?, e: Throwable?) =
61 | output(message, e, LogKind.INFO)
62 |
63 | public override fun warning0(message: String?, e: Throwable?) =
64 | output(message, e, LogKind.WARNING)
65 |
66 | public override fun error0(message: String?, e: Throwable?) =
67 | output(message, e, LogKind.ERROR)
68 |
69 | public override fun debug0(message: String?, e: Throwable?) =
70 | output(message, e, LogKind.DEBUG)
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/impl/MiraiComposeDataStore.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console.impl
2 |
3 | import net.mamoe.mirai.console.data.MultiFilePluginDataStorage
4 | import net.mamoe.mirai.console.data.PluginConfig
5 | import net.mamoe.mirai.console.data.PluginData
6 | import net.mamoe.mirai.console.data.PluginDataHolder
7 |
8 |
9 | internal class ReadablePluginDataStorageImpl(
10 | private val storage: MultiFilePluginDataStorage
11 | ) : MultiFilePluginDataStorage by storage, ReadablePluginDataStorage {
12 | override val dataMap = mutableMapOf>()
13 |
14 | override fun load(holder: PluginDataHolder, instance: PluginData) {
15 | storage.load(holder, instance)
16 | dataMap.getOrPut(holder, ::mutableListOf).add(instance)
17 | }
18 |
19 | override fun store(holder: PluginDataHolder, instance: PluginData) {
20 | storage.store(holder, instance)
21 | }
22 | }
23 |
24 | internal class ReadablePluginConfigStorageImpl(
25 | private val storage: MultiFilePluginDataStorage
26 | ) : MultiFilePluginDataStorage by storage, ReadablePluginConfigStorage {
27 | override val dataMap = mutableMapOf>()
28 |
29 | override fun load(holder: PluginDataHolder, instance: PluginData) {
30 | storage.load(holder, instance)
31 | dataMap.getOrPut(holder, ::mutableListOf).add(instance as PluginConfig)
32 | }
33 |
34 | override fun store(holder: PluginDataHolder, instance: PluginData) {
35 | storage.store(holder, instance as PluginConfig)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/impl/MiraiComposeImpl.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console.impl
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import com.youngerhousea.mirai.compose.console.MiraiComposeImplementation
6 | import com.youngerhousea.mirai.compose.console.MiraiComposeLoginSolver
7 | import com.youngerhousea.mirai.compose.console.Solver
8 | import kotlinx.coroutines.CancellationException
9 | import kotlinx.coroutines.CoroutineExceptionHandler
10 | import kotlinx.coroutines.CoroutineName
11 | import kotlinx.coroutines.SupervisorJob
12 | import mirai_compose.app.BuildConfig
13 | import net.mamoe.mirai.console.MiraiConsole
14 | import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
15 | import net.mamoe.mirai.console.MiraiConsoleImplementation
16 | import net.mamoe.mirai.console.command.CommandManager
17 | import net.mamoe.mirai.console.data.PluginConfig
18 | import net.mamoe.mirai.console.data.PluginData
19 | import net.mamoe.mirai.console.data.PluginDataHolder
20 | import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
21 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
22 | import net.mamoe.mirai.console.util.ConsoleInput
23 | import net.mamoe.mirai.console.util.SemVersion
24 | import net.mamoe.mirai.message.data.Message
25 | import net.mamoe.mirai.utils.BotConfiguration
26 | import net.mamoe.mirai.utils.LoginSolver
27 | import net.mamoe.mirai.utils.MiraiInternalApi
28 | import net.mamoe.mirai.utils.MiraiLogger
29 | import java.nio.file.Path
30 | import java.nio.file.Paths
31 | import kotlin.coroutines.CoroutineContext
32 | import kotlin.io.path.div
33 |
34 | /**
35 | * Create a [MiraiComposeImplementation], this implementation for [MiraiConsoleImplementation]
36 | *
37 | */
38 | val MiraiCompose: MiraiComposeImplementation by lazy { MiraiComposeImpl() }
39 |
40 | internal class MiraiComposeImpl : MiraiComposeImplementation, Solver by MiraiComposeLoginSolver {
41 |
42 | override val composeLogger: MiraiLogger by lazy { createLogger("compose") }
43 |
44 | override val rootPath: Path = Paths.get(System.getProperty("user.dir", ".")).toAbsolutePath()
45 |
46 | override val builtInPluginLoaders = listOf(lazy { JvmPluginLoader })
47 |
48 | override val commandManager: CommandManager by lazy { backendAccess.createDefaultCommandManager(coroutineContext) }
49 |
50 | override val frontEndDescription = MiraiComposeDescription
51 |
52 | override val jvmPluginLoader: JvmPluginLoader by lazy { backendAccess.createDefaultJvmPluginLoader(coroutineContext) }
53 |
54 | override val dataStorageForJvmPluginLoader = ReadablePluginDataStorage(rootPath / "data")
55 |
56 | override val dataStorageForBuiltIns = ReadablePluginDataStorage(rootPath / "data")
57 |
58 | override val configStorageForJvmPluginLoader = ReadablePluginConfigStorage(rootPath / "config")
59 |
60 | override val configStorageForBuiltIns = ReadablePluginConfigStorage(rootPath / "config")
61 |
62 | override val consoleInput: ConsoleInput = MiraiComposeInput
63 |
64 | override val consoleCommandSender = MiraiConsoleSender(composeLogger)
65 |
66 | override val consoleDataScope: MiraiConsoleImplementation.ConsoleDataScope by lazy {
67 | MiraiConsoleImplementation.ConsoleDataScope.createDefault(
68 | coroutineContext,
69 | dataStorageForBuiltIns,
70 | configStorageForBuiltIns
71 | )
72 | }
73 |
74 | @OptIn(MiraiInternalApi::class)
75 | override fun createLogger(identity: String?): MiraiLogger =
76 | MiraiComposeLogger(identity = identity ?: "Default", output = { content, exception, priority ->
77 | val newContent =
78 | if (exception != null) (content ?: exception.toString()) + "\n${exception.stackTraceToString()}"
79 | else content.toString()
80 | val trueContent = "$currentTimeFormatted ${priority.simpleName}/$identity $newContent"
81 |
82 | printForDebug(priority, trueContent)
83 | storeInLogStorage(priority, trueContent)
84 | })
85 |
86 | private fun storeInLogStorage(priority: LogKind, content: String) {
87 | logStorage.value += Log(content, priority)
88 | }
89 |
90 | private fun printForDebug(priority: LogKind, content: String) {
91 | println("${priority.textColor}${content}${TextColor.RESET}")
92 | }
93 |
94 | override val JvmPlugin.data: List
95 | get() = if (this is PluginDataHolder) dataStorageForJvmPluginLoader[this] else error("Plugin is Not Holder!")
96 |
97 | override val JvmPlugin.config: List
98 | get() = if (this is PluginDataHolder) configStorageForJvmPluginLoader[this] else error("Plugin is Not Holder!")
99 |
100 | override val logStorage: MutableState> = mutableStateOf(listOf())
101 |
102 | override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver = MiraiComposeLoginSolver
103 |
104 | override val coroutineContext: CoroutineContext =
105 | CoroutineName("MiraiCompose") + CoroutineExceptionHandler { coroutineContext, throwable ->
106 | if (throwable is CancellationException) {
107 | return@CoroutineExceptionHandler
108 | }
109 | val coroutineName = coroutineContext[CoroutineName]?.name ?: ""
110 | MiraiConsole.mainLogger.error("Exception in coroutine $coroutineName", throwable)
111 | } + SupervisorJob()
112 |
113 | }
114 |
115 | object MiraiComposeDescription : MiraiConsoleFrontEndDescription {
116 | override val name: String = BuildConfig.projectName
117 | override val vendor: String = BuildConfig.projectGroup
118 | override val version: SemVersion = SemVersion(BuildConfig.projectVersion)
119 | }
120 |
121 | object MiraiComposeInput : ConsoleInput {
122 | override suspend fun requestInput(hint: String): String {
123 | error("Not yet implemented")
124 | }
125 | }
126 |
127 |
128 | class MiraiConsoleSender(
129 | private val logger: MiraiLogger
130 | ) : MiraiConsoleImplementation.ConsoleCommandSenderImpl {
131 | override suspend fun sendMessage(message: String) {
132 | logger.info(message)
133 | }
134 |
135 | override suspend fun sendMessage(message: Message) {
136 | sendMessage(message.toString())
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/impl/ReadablePluginStorage.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console.impl
2 |
3 | import net.mamoe.mirai.console.data.MultiFilePluginDataStorage
4 | import net.mamoe.mirai.console.data.PluginConfig
5 | import net.mamoe.mirai.console.data.PluginData
6 | import net.mamoe.mirai.console.data.PluginDataHolder
7 | import java.nio.file.Path
8 |
9 | /**
10 | * 可读的数据存储,由[MultiFilePluginDataStorage]代理实现
11 | */
12 | interface ReadablePluginDataStorage : MultiFilePluginDataStorage {
13 | val dataMap: Map>
14 | }
15 |
16 | operator fun ReadablePluginDataStorage.get(pluginDataHolder: PluginDataHolder): List =
17 | dataMap[pluginDataHolder] ?: emptyList()
18 |
19 | /**
20 | * 创建一个 [ReadablePluginDataStorage] 实例,实现为[MultiFilePluginDataStorage].
21 | */
22 | fun ReadablePluginDataStorage(directory: Path): ReadablePluginDataStorage =
23 | ReadablePluginDataStorageImpl(MultiFilePluginDataStorage(directory))
24 |
25 |
26 |
27 | /**
28 | * 可读的配置存储,由[MultiFilePluginDataStorage]代理实现
29 | */
30 | interface ReadablePluginConfigStorage : MultiFilePluginDataStorage {
31 | val dataMap: Map>
32 | }
33 |
34 | operator fun ReadablePluginConfigStorage.get(pluginDataHolder: PluginDataHolder): List =
35 | dataMap[pluginDataHolder] ?: emptyList()
36 |
37 | /**
38 | * 创建一个 [ReadablePluginConfigStorage] 实例,实现为[MultiFilePluginDataStorage].
39 | */
40 | fun ReadablePluginConfigStorage(directory: Path): ReadablePluginConfigStorage =
41 | ReadablePluginConfigStorageImpl(MultiFilePluginDataStorage(directory))
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/console/impl/ViewModelStoreImpl.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console.impl
2 |
3 | import com.youngerhousea.mirai.compose.console.ViewModel
4 | import com.youngerhousea.mirai.compose.console.ViewModelStore
5 | import java.util.concurrent.ConcurrentHashMap
6 |
7 | internal class ViewModelStoreImpl : ViewModelStore {
8 | private val hashMap: ConcurrentHashMap = ConcurrentHashMap()
9 |
10 | override fun put(key: Any, viewModel: ViewModel) {
11 | hashMap[key]?.onDestroy()
12 |
13 | hashMap[key] = viewModel
14 | }
15 |
16 | override fun get(key: Any): ViewModel? {
17 | return hashMap[key]
18 | }
19 |
20 | override fun clean() {
21 | hashMap.values.forEach(ViewModel::onDestroy)
22 | hashMap.clear()
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/model/LoginCredential.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LoginCredential(
7 | val account: String = "",
8 | val password: String = "",
9 | val passwordKind: PasswordKind = PasswordKind.PLAIN,
10 | val protocolKind: ProtocolKind = ProtocolKind.ANDROID_PHONE,
11 | ) {
12 |
13 | @Serializable
14 | enum class ProtocolKind {
15 | ANDROID_PHONE,
16 | ANDROID_PAD,
17 | ANDROID_WATCH
18 | }
19 |
20 | @Serializable
21 | enum class PasswordKind {
22 | PLAIN,
23 | MD5
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/resource/R.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.resource
2 |
3 | import androidx.compose.material.Colors
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Add
6 | import androidx.compose.material.icons.filled.KeyboardArrowLeft
7 | import androidx.compose.material.icons.filled.Search
8 | import androidx.compose.material.icons.outlined.*
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.ImageBitmap
11 | import androidx.compose.ui.graphics.painter.Painter
12 | import androidx.compose.ui.graphics.vector.ImageVector
13 | import androidx.compose.ui.res.loadImageBitmap
14 | import androidx.compose.ui.res.loadSvgPainter
15 | import androidx.compose.ui.res.loadXmlImageVector
16 | import androidx.compose.ui.res.useResource
17 | import androidx.compose.ui.unit.Density
18 | import org.xml.sax.InputSource
19 |
20 | object R {
21 | object Version {
22 | const val Frontend = "https://github.com/sonder-joker/mirai-compose"
23 | const val Backend = "https://github.com/mamoe/mirai"
24 | }
25 |
26 | object String {
27 | const val BotMenuExit = "退出"
28 | const val BotMenuAdd = "添加"
29 | const val RailTabFirst = "Message"
30 | const val RailTabSecond = "Plugin"
31 | const val RailTabThird = "Setting"
32 | const val RailTabFourth = "About"
33 | const val RailTabFive = "Log"
34 | const val EditSuccess = "Success"
35 | const val EditFailure = "Failure"
36 |
37 | object Plugin {
38 | const val Add = "添加插件"
39 | }
40 |
41 | const val PasswordError = "密码错误"
42 | const val RetryLater = "请稍后再试"
43 | const val NotSupportSlider = "目前不支持滑动输入框"
44 | const val SMSLoginError = "Mirai暂未提供短信输入"
45 | const val NoStandInput = "无标准输入"
46 | const val NoServerError = "无可用服务器"
47 | const val IllPassword = "密码长度最多为16"
48 | const val UnknownError = "Unknown Reason"
49 | const val CancelLogin = ""
50 |
51 | const val Login = "Login"
52 | const val Password = "Password"
53 | }
54 |
55 | object Image {
56 | val Java = loadXmlImageVector("ic_java.xml")
57 | val Kotlin = loadXmlImageVector("ic_kotlin.xml")
58 | val Mirai = loadXmlImageVector("ic_mirai.xml")
59 | }
60 |
61 | object Icon {
62 | val Search = Icons.Default.Search
63 | val ConsoleLog = Icons.Outlined.Description
64 | val Message = Icons.Outlined.Message
65 | val Plugins = Icons.Outlined.Extension
66 | val Setting = Icons.Outlined.Settings
67 | val About = Icons.Outlined.Forum
68 | val Add = Icons.Filled.Add
69 | val Back = Icons.Default.KeyboardArrowLeft
70 | val Mirai = loadImageBitmap("mirai.png")
71 | }
72 |
73 | object Colors {
74 | val SearchText = Color.Yellow
75 | val TopAppBar = Color(235, 235, 235)
76 | val SplitLeft = Color(245, 245, 245)
77 | }
78 |
79 | }
80 |
81 |
82 | val color = Colors(
83 | primary = Color(0xFF00b0ff),
84 | primaryVariant = Color(0xFF69e2ff),
85 | secondary = Color(0xFF03DAC6),
86 | secondaryVariant = Color(0xFF018786),
87 | background = Color(0xFFFFFFFF),
88 | surface = Color(0xFFFFFFFF),
89 | error = Color(0xFFB00020),
90 | onPrimary = Color(0xFFFFFFFF),
91 | onSecondary = Color(0xFF000000),
92 | onBackground = Color(0xFF000000),
93 | onSurface = Color(0xFF000000),
94 | onError = Color(0xFFFFFFFF),
95 | isLight = true
96 | )
97 |
98 |
99 | fun loadImageBitmap(path: String): ImageBitmap =
100 | useResource(path) { it.buffered().use(::loadImageBitmap) }
101 |
102 | @Suppress("unused")
103 | fun loadSvgPainter(path: String, density: Density = Density(1f)): Painter =
104 | useResource(path) { loadSvgPainter(it, density) }
105 |
106 | fun loadXmlImageVector(path: String, density: Density = Density(1f)): ImageVector =
107 | useResource(path) { stream -> stream.buffered().use { loadXmlImageVector(InputSource(it), density) } }
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/BotItem.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.shape.CircleShape
5 | import androidx.compose.material.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.CompositionLocalProvider
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.clipToBounds
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.ImageBitmap
13 | import androidx.compose.ui.graphics.painter.BitmapPainter
14 | import androidx.compose.ui.res.loadImageBitmap
15 | import androidx.compose.ui.text.font.FontWeight
16 | import com.youngerhousea.mirai.compose.ui.login.AsyncImage
17 | import io.ktor.client.request.*
18 | import net.mamoe.mirai.Bot
19 | import net.mamoe.mirai.Mirai
20 |
21 | @Composable
22 | fun BotItem(
23 | bot: Bot,
24 | ) {
25 | BotContent(
26 | avatar = {
27 | AsyncImage(load = {
28 | loadImageBitmap(Mirai.Http.get(bot.avatarUrl))
29 | }, painterFor = {
30 | BitmapPainter(it)
31 | }, null)
32 | },
33 | nick = bot.run {
34 | try {
35 | nick
36 | } catch (e: UninitializedPropertyAccessException) {
37 | "Unknown"
38 | }
39 | },
40 | id = bot.id.toString()
41 | )
42 | }
43 |
44 | @Composable
45 | fun EmptyBotItem() {
46 | BotContent(
47 | avatar = {
48 | ImageBitmap(200, 200)
49 | },
50 | nick = "NoLogin",
51 | id = "Unknown"
52 | )
53 | }
54 |
55 |
56 | @Composable
57 | fun BotContent(
58 | modifier: Modifier = Modifier,
59 | avatar: @Composable () -> Unit,
60 | nick: String,
61 | id: String
62 | ) {
63 | Row(
64 | modifier = modifier
65 | .aspectRatio(2f)
66 | .clipToBounds(),
67 | horizontalArrangement = Arrangement.SpaceEvenly,
68 | verticalAlignment = Alignment.CenterVertically
69 | ) {
70 | Spacer(Modifier.weight(1f).fillMaxHeight())
71 | Surface(
72 | modifier = Modifier
73 | .weight(3f, fill = false),
74 | shape = CircleShape,
75 | color = Color(0xff979595),
76 | ) {
77 | avatar()
78 | }
79 | Column(
80 | Modifier
81 | .weight(6f),
82 | horizontalAlignment = Alignment.CenterHorizontally
83 | ) {
84 | Text(nick, fontWeight = FontWeight.Bold, maxLines = 1)
85 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
86 | Text(id, style = MaterialTheme.typography.body2)
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/HostPage.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui
2 |
3 | import androidx.compose.desktop.ui.tooling.preview.Preview
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.material.Icon
10 | import androidx.compose.material.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 | import com.youngerhousea.mirai.compose.console.viewModel
17 | import com.youngerhousea.mirai.compose.resource.R
18 | import com.youngerhousea.mirai.compose.ui.about.About
19 | import com.youngerhousea.mirai.compose.ui.log.ConsoleLog
20 | import com.youngerhousea.mirai.compose.ui.message.BotMessage
21 | import com.youngerhousea.mirai.compose.ui.message.Message
22 | import com.youngerhousea.mirai.compose.ui.plugins.Plugins
23 | import com.youngerhousea.mirai.compose.ui.setting.Setting
24 | import com.youngerhousea.mirai.compose.viewmodel.Host
25 | import com.youngerhousea.mirai.compose.viewmodel.HostViewModel
26 | import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
27 | import org.jetbrains.compose.splitpane.HorizontalSplitPane
28 |
29 | @Composable
30 | fun HostPage() {
31 | NavHost()
32 | }
33 |
34 | @OptIn(ExperimentalSplitPaneApi::class)
35 | @Composable
36 | fun NavHost(
37 | hostViewModel: Host = viewModel { HostViewModel() }
38 | ) {
39 | val state by hostViewModel.state
40 |
41 | HorizontalSplitPane {
42 | first(minSize = MinFirstSize) {
43 | NavHostFirst(
44 | modifier = Modifier.background(R.Colors.SplitLeft),
45 | navigate = state.navigate,
46 | onRouteMessage = { hostViewModel.dispatch(Host.Route.Message) },
47 | onRoutePlugins = { hostViewModel.dispatch(Host.Route.Plugins) },
48 | onRouteSetting = { hostViewModel.dispatch(Host.Route.Setting) },
49 | onRouteAbout = { hostViewModel.dispatch(Host.Route.About) },
50 | onRouteConsoleLog = { hostViewModel.dispatch(Host.Route.ConsoleLog) }
51 | )
52 | }
53 | second {
54 | NavHostSecond(state.navigate)
55 | }
56 | }
57 | }
58 |
59 | @Composable
60 | fun NavHostSecond(hostRoute: Host.Route) {
61 | when (hostRoute) {
62 | is Host.Route.About -> About()
63 | is Host.Route.Message -> Message()
64 | is Host.Route.Plugins -> Plugins()
65 | is Host.Route.Setting -> Setting()
66 | is Host.Route.BotMessage -> BotMessage()
67 | is Host.Route.ConsoleLog -> ConsoleLog()
68 | }
69 | }
70 |
71 | @Composable
72 | fun NavHostFirst(
73 | modifier: Modifier = Modifier,
74 | navigate: Host.Route,
75 | onRouteMessage: () -> Unit,
76 | onRoutePlugins: () -> Unit,
77 | onRouteSetting: () -> Unit,
78 | onRouteAbout: () -> Unit,
79 | onRouteConsoleLog: () -> Unit,
80 | ) {
81 | Column(
82 | modifier.fillMaxSize(),
83 | verticalArrangement = Arrangement.Top,
84 | ) {
85 | NavHostFirstBotMenu()
86 | RailTab(
87 | onClick = onRouteMessage,
88 | selected = navigate == Host.Route.Message
89 | ) {
90 | Icon(R.Icon.Message, null)
91 | Text(R.String.RailTabFirst)
92 | }
93 | RailTab(
94 | onClick = onRoutePlugins,
95 | selected = navigate == Host.Route.Plugins,
96 | ) {
97 | Icon(R.Icon.Plugins, null)
98 | Text(R.String.RailTabSecond)
99 | }
100 | RailTab(
101 | onClick = onRouteSetting,
102 | selected = navigate == Host.Route.Setting
103 | ) {
104 | Icon(R.Icon.Setting, null)
105 | Text(R.String.RailTabThird)
106 | }
107 | RailTab(
108 | onClick = onRouteAbout,
109 | selected = navigate == Host.Route.About
110 | ) {
111 | Icon(R.Icon.About, null)
112 | Text(R.String.RailTabFourth)
113 | }
114 | RailTab(
115 | onClick = onRouteConsoleLog,
116 | selected = navigate == Host.Route.ConsoleLog
117 | ) {
118 | Icon(R.Icon.ConsoleLog, null)
119 | Text(R.String.RailTabFive)
120 | }
121 | }
122 | }
123 |
124 | private val MinFirstSize = 170.dp
125 |
126 | @Preview
127 | @Composable
128 | fun NavHostFirstPreview() {
129 | Column(
130 | Modifier.fillMaxWidth(),
131 | verticalArrangement = Arrangement.Top,
132 | horizontalAlignment = Alignment.Start
133 | ) {
134 | RailTab(
135 | onClick = {},
136 | selected = true
137 | ) {
138 | Icon(R.Icon.Message, null)
139 | Text(R.String.RailTabFirst)
140 | }
141 | RailTab(
142 | onClick = {},
143 | selected = false,
144 | ) {
145 | Icon(R.Icon.Plugins, null)
146 | Text(R.String.RailTabSecond)
147 | }
148 | RailTab(
149 | onClick = {},
150 | selected = false
151 | ) {
152 | Icon(R.Icon.Setting, null)
153 | Text(R.String.RailTabThird)
154 | }
155 | RailTab(
156 | onClick = {},
157 | selected = false
158 | ) {
159 | Icon(R.Icon.About, null)
160 | Text(R.String.RailTabFourth)
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/NavHostBotMenu.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.DropdownMenu
6 | import androidx.compose.material.DropdownMenuItem
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 | import com.youngerhousea.mirai.compose.MiraiComposeDialog
14 | import com.youngerhousea.mirai.compose.console.viewModel
15 | import com.youngerhousea.mirai.compose.resource.R
16 | import com.youngerhousea.mirai.compose.ui.login.LoginInterface
17 | import com.youngerhousea.mirai.compose.viewmodel.Host
18 | import com.youngerhousea.mirai.compose.viewmodel.HostViewModel
19 | import net.mamoe.mirai.Bot
20 |
21 | @Composable
22 | fun NavHostFirstBotMenu(
23 | hostViewModel: Host = viewModel { HostViewModel() },
24 | ) {
25 | val state by hostViewModel.state
26 |
27 | NavHostFirstBotMenuContent(
28 | isExpand = state.menuExpand,
29 | botList = state.botList,
30 | currentBot = state.currentBot,
31 | onAvatarBoxClick = { hostViewModel.dispatch(Host.Action.OpenMenu) },
32 | dismissExpandMenu = { hostViewModel.dispatch(Host.Action.CloseMenu) },
33 | onAddNewBotButtonClick = {
34 | hostViewModel.dispatch(Host.Action.CloseMenu)
35 | hostViewModel.dispatch(Host.Action.OpenLoginDialog)
36 | },
37 | onBotItemClick = { hostViewModel.dispatch(Host.Route.BotMessage(it)) }
38 | )
39 | if (state.dialogExpand) {
40 | MiraiComposeDialog({
41 | hostViewModel.dispatch(Host.Action.CloseLoginDialog)
42 | }) {
43 | LoginInterface()
44 | }
45 | }
46 | }
47 |
48 | @Composable
49 | fun NavHostFirstBotMenuContent(
50 | isExpand: Boolean,
51 | botList: List,
52 | currentBot: Bot?,
53 | onAvatarBoxClick: () -> Unit,
54 | dismissExpandMenu: () -> Unit,
55 | onAddNewBotButtonClick: () -> Unit,
56 | onBotItemClick: (Bot) -> Unit
57 | ) {
58 | Box(Modifier.height(80.dp).fillMaxWidth()) {
59 | Row(
60 | modifier = Modifier
61 | .fillMaxSize()
62 | .clickable(onClick = onAvatarBoxClick),
63 | horizontalArrangement = Arrangement.Start,
64 | verticalAlignment = Alignment.CenterVertically,
65 | ) {
66 | if (currentBot == null)
67 | EmptyBotItem()
68 | else
69 | BotItem(currentBot)
70 | }
71 |
72 | DropdownMenu(isExpand, onDismissRequest = dismissExpandMenu) {
73 | DropdownMenuItem(onClick = dismissExpandMenu) {
74 | Text(R.String.BotMenuExit)
75 | }
76 |
77 | DropdownMenuItem(onClick = onAddNewBotButtonClick) {
78 | Text(R.String.BotMenuAdd)
79 | }
80 |
81 | botList.forEach { bot ->
82 | DropdownMenuItem(onClick = {
83 | onBotItemClick(bot)
84 | }) {
85 | BotItem(bot)
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/RailTab.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material.LocalContentColor
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 |
16 |
17 | @Composable
18 | fun RailTab(
19 | modifier: Modifier = Modifier,
20 | onClick: () -> Unit,
21 | selected: Boolean = false,
22 | content: @Composable RowScope.() -> Unit,
23 | ) {
24 | val color by animateColorAsState(if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
25 |
26 | CompositionLocalProvider(LocalContentColor provides color) {
27 | Row(
28 | modifier = modifier.height(RailTabHeight).fillMaxWidth().clickable(onClick = onClick),
29 | verticalAlignment = Alignment.CenterVertically,
30 | horizontalArrangement = Arrangement.Center,
31 | content = content
32 | )
33 | }
34 | }
35 |
36 | private val RailTabHeight = 80.dp
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui
2 |
3 | import androidx.compose.material.icons.materialPath
4 | import androidx.compose.ui.graphics.vector.ImageVector
5 | import androidx.compose.ui.unit.dp
6 |
7 | fun closeIcon(size: Float = 24f, viewport: Float = 24f) =
8 | ImageVector.Builder(
9 | name = "",
10 | defaultWidth = size.dp,
11 | defaultHeight = size.dp,
12 | viewportWidth = viewport,
13 | viewportHeight = viewport
14 | ).materialPath {
15 | moveTo(12f, 10f)
16 | lineTo(2f, 0f)
17 | lineTo(0f, 2f)
18 | lineTo(10f, 12f)
19 | lineTo(0f, 22f)
20 | lineTo(2f, 24f)
21 | lineTo(12f, 14f)
22 | lineTo(22f, 24f)
23 | lineTo(24f, 22f)
24 | lineTo(14f, 12f)
25 | lineTo(24f, 2f)
26 | lineTo(22f, 0f)
27 | lineTo(12f, 10f)
28 | }.build()
29 |
30 | fun minIcon(size: Float = 24f, viewport: Float = 24f) =
31 | ImageVector.Builder(
32 | name = "",
33 | defaultWidth = size.dp,
34 | defaultHeight = size.dp,
35 | viewportWidth = viewport,
36 | viewportHeight = viewport
37 | ).materialPath {
38 | moveTo(2f, 14f)
39 | lineTo(22f, 14f)
40 | lineTo(22f, 10f)
41 | lineTo(2f, 10f)
42 | close()
43 | }.build()
44 |
45 | fun maxIcon(size: Float = 24f, viewport: Float = 24f) =
46 | ImageVector.Builder(
47 | name = "",
48 | defaultWidth = size.dp,
49 | defaultHeight = size.dp,
50 | viewportWidth = viewport,
51 | viewportHeight = viewport
52 | ).materialPath {
53 | moveTo(2f, 2f)
54 | lineTo(20f, 2f)
55 | lineTo(20f, 5f)
56 | lineTo(2f, 5f)
57 | lineTo(2f, 2f)
58 |
59 | moveTo(20f, 2f)
60 | lineTo(20f, 20f)
61 | lineTo(17f, 20f)
62 | lineTo(17f, 2f)
63 |
64 | moveTo(19f, 20f)
65 | lineTo(2f, 20f)
66 | lineTo(2f, 17f)
67 | lineTo(19f, 17f)
68 |
69 | moveTo(2f, 20f)
70 | lineTo(2f, 2f)
71 | lineTo(5f, 2f)
72 | lineTo(5f, 20f)
73 | }.build()
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/about/About.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.about
2 |
3 | import androidx.compose.desktop.ui.tooling.preview.Preview
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.text.ClickableText
10 | import androidx.compose.foundation.text.selection.SelectionContainer
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.text.AnnotatedString
16 | import androidx.compose.ui.text.SpanStyle
17 | import androidx.compose.ui.text.buildAnnotatedString
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.text.withStyle
20 | import com.youngerhousea.mirai.compose.resource.R
21 | import net.mamoe.mirai.console.MiraiConsole
22 | import java.awt.Desktop
23 | import java.net.URI
24 |
25 | @Composable
26 | fun About(
27 | ) {
28 | Row(
29 | Modifier.fillMaxSize(),
30 | horizontalArrangement = Arrangement.Center,
31 | verticalAlignment = Alignment.CenterVertically
32 | ) {
33 | Image(R.Image.Mirai, "mirai")
34 | Column {
35 | ClickableUrlText(frontendAnnotatedString(R.Version.Frontend))
36 | ClickableUrlText(backendAnnotatedString(R.Version.Backend))
37 | }
38 | }
39 | }
40 |
41 | @Composable
42 | private fun frontendAnnotatedString(frontend: String) = remember(frontend) {
43 | buildAnnotatedString {
44 | pushStringAnnotation(
45 | tag = "URL",
46 | annotation = frontend,
47 | )
48 | withStyle(
49 | style = SpanStyle(
50 | fontWeight = FontWeight.Bold
51 | )
52 | ) {
53 | append(frontend)
54 | }
55 | pop()
56 | }
57 | }
58 |
59 | @Composable
60 | private fun backendAnnotatedString(backEnd: String) = remember(backEnd) {
61 | buildAnnotatedString {
62 | pushStringAnnotation(
63 | tag = "URL",
64 | annotation = backEnd,
65 | )
66 | withStyle(
67 | style = SpanStyle(
68 | fontWeight = FontWeight.Bold
69 | )
70 | ) {
71 | append("Backend V ${MiraiConsole.version}")
72 | }
73 | pop()
74 | }
75 | }
76 |
77 | @Composable
78 | fun ClickableUrlText(annotatedString: AnnotatedString) {
79 | SelectionContainer {
80 | ClickableText(annotatedString) { offset ->
81 | annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset).firstOrNull()
82 | ?.let { annotation ->
83 | Desktop.getDesktop().browse(URI(annotation.item))
84 | }
85 | }
86 | }
87 | }
88 |
89 | @Preview
90 | @Composable
91 | fun AboutPreview() {
92 | About()
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/log/ConsoleLog.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.log
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.VerticalScrollbar
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.foundation.lazy.rememberLazyListState
9 | import androidx.compose.foundation.rememberScrollbarAdapter
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.foundation.text.selection.SelectionContainer
12 | import androidx.compose.material.*
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.ExperimentalComposeUiApi
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.input.key.*
22 | import androidx.compose.ui.text.AnnotatedString
23 | import androidx.compose.ui.text.SpanStyle
24 | import androidx.compose.ui.unit.dp
25 | import com.youngerhousea.mirai.compose.console.impl.Log
26 | import com.youngerhousea.mirai.compose.console.impl.MiraiCompose
27 | import com.youngerhousea.mirai.compose.console.viewModel
28 | import com.youngerhousea.mirai.compose.resource.R
29 | import com.youngerhousea.mirai.compose.viewmodel.ConsoleLog
30 | import com.youngerhousea.mirai.compose.viewmodel.ConsoleLogViewModel
31 | import java.util.*
32 |
33 | @OptIn(ExperimentalComposeUiApi::class)
34 | @Composable
35 | fun ConsoleLog(consoleLog: ConsoleLog = viewModel { ConsoleLogViewModel() }) {
36 | val state by consoleLog.state
37 | val log by MiraiCompose.logStorage
38 |
39 | Scaffold(
40 | modifier = Modifier.onPreviewCtrlFDown { consoleLog.dispatch(ConsoleLog.Action.SetSearchBar) },
41 | topBar = {
42 | if (state.searchBarVisible)
43 | TextField(
44 | value = state.searchContent,
45 | onValueChange = { consoleLog.dispatch(ConsoleLog.Action.UpdateSearchContent(it)) },
46 | leadingIcon = { Icon(R.Icon.Search, null) },
47 | modifier = Modifier
48 | .fillMaxWidth()
49 | .padding(30.dp)
50 | .animateContentSize(),
51 | shape = RoundedCornerShape(15.dp),
52 | colors = TextFieldDefaults.textFieldColors(
53 | focusedIndicatorColor = Color.Transparent,
54 | unfocusedIndicatorColor = Color.Transparent
55 | )
56 | )
57 | }, floatingActionButton = {
58 | // FloatingActionButton(onClick = {
59 | //
60 | // }) {
61 | // }
62 | }) {
63 | Column(Modifier.padding(it)) {
64 | LogBox(
65 | Modifier
66 | .fillMaxSize()
67 | .weight(8f)
68 | .padding(horizontal = 40.dp, vertical = 20.dp),
69 | logs = log,
70 | searchText = state.searchContent,
71 | )
72 | CommandSendBox(
73 | command = state.currentCommand,
74 | onCommandChange = { commandContent ->
75 | consoleLog.dispatch(
76 | ConsoleLog.Action.UpdateCurrentCommand(
77 | commandContent
78 | )
79 | )
80 | },
81 | onClick = { consoleLog.dispatch(ConsoleLog.Action.EnterCommand) },
82 | modifier = Modifier
83 | .weight(1f)
84 | .padding(horizontal = 40.dp),
85 | )
86 | }
87 | }
88 | }
89 |
90 | @OptIn(ExperimentalComposeUiApi::class)
91 | internal fun Modifier.onPreviewCtrlFDown(action: () -> Unit): Modifier = onPreviewKeyEvent {
92 | if (it.isCtrlPressed && it.key == Key.F && it.type == KeyEventType.KeyDown) {
93 | action()
94 | true
95 | } else false
96 | }
97 |
98 | @Composable
99 | internal fun LogBox(
100 | modifier: Modifier = Modifier,
101 | logs: List,
102 | searchText: String,
103 | ) {
104 | val lazyListState = rememberLazyListState()
105 |
106 | val renderLog = remember(logs, searchText) { logs.map { it.annotatedString(searchText) } }
107 |
108 | Box(modifier) {
109 | SelectionContainer {
110 | LazyColumn(state = lazyListState, modifier = Modifier.animateContentSize()) {
111 | items(renderLog) { adaptiveLog ->
112 | Text(adaptiveLog)
113 | }
114 | }
115 | }
116 |
117 | VerticalScrollbar(
118 | modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
119 | adapter = rememberScrollbarAdapter(scrollState = lazyListState)
120 | )
121 | }
122 |
123 | LaunchedEffect(logs.size) {
124 | if (logs.isNotEmpty())
125 | lazyListState.animateScrollToItem(logs.size)
126 | }
127 | }
128 |
129 |
130 | private fun Log.annotatedString(
131 | searchText: String,
132 | ): AnnotatedString {
133 | val builder = AnnotatedString.Builder()
134 | if (searchText.isEmpty())
135 | return with(builder) {
136 | append(
137 | AnnotatedString(
138 | message,
139 | spanStyle = SpanStyle(
140 | color = kind.color
141 | ),
142 | )
143 | )
144 | toAnnotatedString()
145 | }
146 | else
147 | return with(builder) {
148 | val tok = StringTokenizer(message, searchText, true)
149 | while (tok.hasMoreTokens()) {
150 | val next = tok.nextToken()
151 | if (next == searchText) {
152 | append(
153 | AnnotatedString(
154 | next,
155 | spanStyle = SpanStyle(
156 | color = R.Colors.SearchText
157 | ),
158 | )
159 | )
160 | } else {
161 | append(
162 | AnnotatedString(
163 | next,
164 | spanStyle = SpanStyle(
165 | kind.color
166 | ),
167 | )
168 | )
169 | }
170 | }
171 | toAnnotatedString()
172 | }
173 | }
174 |
175 | @OptIn(ExperimentalComposeUiApi::class)
176 | @Composable
177 | internal fun CommandSendBox(
178 | command: String,
179 | onCommandChange: (String) -> Unit,
180 | modifier: Modifier = Modifier,
181 | onClick: () -> Unit
182 | ) {
183 | Row(
184 | modifier = modifier,
185 | verticalAlignment = Alignment.CenterVertically
186 | ) {
187 | OutlinedTextField(
188 | value = command,
189 | onValueChange = onCommandChange,
190 | singleLine = true,
191 | modifier = Modifier
192 | .weight(13f)
193 | .onPreviewEnterDown(action = onClick)
194 | )
195 |
196 | Spacer(
197 | Modifier.weight(1f)
198 | )
199 |
200 | Button(
201 | onClick = onClick,
202 | modifier = Modifier
203 | .weight(2f),
204 | ) {
205 | Text("Send")
206 | }
207 | }
208 | }
209 |
210 | internal fun Modifier.onPreviewEnterDown(action: () -> Unit): Modifier = onPreviewKeyEvent {
211 | if (it.isEnterDown) {
212 | action()
213 | true
214 | } else false
215 | }
216 |
217 | @OptIn(ExperimentalComposeUiApi::class)
218 | internal val KeyEvent.isEnterDown
219 | get() = key == Key.Enter && type == KeyEventType.KeyDown
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/login/LoginDialog.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.login
2 |
3 | import androidx.compose.animation.core.*
4 | import androidx.compose.foundation.Canvas
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.text.KeyboardOptions
9 | import androidx.compose.material.*
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.*
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.geometry.Offset
16 | import androidx.compose.ui.geometry.Size
17 | import androidx.compose.ui.graphics.SolidColor
18 | import androidx.compose.ui.graphics.painter.Painter
19 | import androidx.compose.ui.layout.ContentScale
20 | import androidx.compose.ui.layout.onGloballyPositioned
21 | import androidx.compose.ui.platform.LocalDensity
22 | import androidx.compose.ui.text.input.ImeAction
23 | import androidx.compose.ui.text.input.KeyboardType
24 | import androidx.compose.ui.text.input.PasswordVisualTransformation
25 | import androidx.compose.ui.text.input.VisualTransformation
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.toSize
28 | import com.youngerhousea.mirai.compose.console.UICannotFinish
29 | import com.youngerhousea.mirai.compose.resource.R
30 | import com.youngerhousea.mirai.compose.ui.log.onPreviewEnterDown
31 | import io.ktor.utils.io.*
32 | import kotlinx.coroutines.Dispatchers
33 | import kotlinx.coroutines.launch
34 | import kotlinx.coroutines.withContext
35 | import net.mamoe.mirai.console.MiraiConsole
36 | import net.mamoe.mirai.network.*
37 | import net.mamoe.mirai.utils.BotConfiguration
38 | import java.io.IOException
39 |
40 | @Composable
41 | fun LoginInterface() {
42 | var account by remember { mutableStateOf("") }
43 | var password by remember { mutableStateOf("") }
44 | var protocol by remember { mutableStateOf(BotConfiguration.MiraiProtocol.ANDROID_PHONE) }
45 | val host = remember { SnackbarHostState() }
46 | var loadingState by remember { mutableStateOf(false) }
47 | val scope = rememberCoroutineScope()
48 |
49 | fun onLogin() {
50 | scope.launch {
51 | val bot = MiraiConsole.addBot(account.toLong(), password) {
52 | this.protocol = protocol
53 | }
54 | try {
55 | loadingState = true
56 | bot.login()
57 | } catch (e: Exception) {
58 | when (e) {
59 | is WrongPasswordException ->
60 | host.showSnackbar(e.message ?: R.String.PasswordError)
61 |
62 | is RetryLaterException -> {
63 | host.showSnackbar(e.message ?: R.String.RetryLater)
64 | }
65 | is UnsupportedSliderCaptchaException -> {
66 | host.showSnackbar(e.message ?: R.String.NotSupportSlider)
67 | }
68 | is UnsupportedSMSLoginException -> {
69 | host.showSnackbar(e.message ?: R.String.SMSLoginError)
70 | }
71 | is NoStandardInputForCaptchaException -> {
72 | host.showSnackbar(e.message ?: R.String.NoStandInput)
73 | }
74 | is NoServerAvailableException -> {
75 | host.showSnackbar(e.message ?: R.String.NoServerError)
76 | }
77 | is IllegalArgumentException -> {
78 | host.showSnackbar(e.message ?: R.String.IllPassword)
79 | }
80 | is CancellationException -> {
81 | host.showSnackbar(e.message ?: R.String.CancelLogin)
82 | }
83 | is UICannotFinish -> {
84 |
85 | }
86 | else -> {
87 | host.showSnackbar(e.message ?: R.String.UnknownError)
88 | }
89 | }
90 | bot.close()
91 | } finally {
92 | loadingState = false
93 | }
94 | }
95 | }
96 |
97 | Scaffold(
98 | scaffoldState = rememberScaffoldState(snackbarHostState = host),
99 | modifier = Modifier.onPreviewEnterDown {
100 | onLogin()
101 | }) {
102 | Column(
103 | verticalArrangement = Arrangement.Center,
104 | horizontalAlignment = Alignment.CenterHorizontally,
105 | modifier = Modifier.fillMaxSize()
106 | ) {
107 | Image(
108 | R.Image.Mirai,
109 | contentDescription = null,
110 | modifier = Modifier
111 | .weight(1f)
112 | .padding(5.dp)
113 | )
114 | AccountTextField(
115 | modifier = Modifier.weight(1f),
116 | account = account,
117 | onAccountTextChange = { account = it },
118 | )
119 | PasswordTextField(
120 | modifier = Modifier.weight(1f),
121 | password = password,
122 | onPasswordTextChange = { password = it },
123 | )
124 |
125 | CheckProtocol(modifier = Modifier.weight(1f), protocol) {
126 | protocol = it
127 | }
128 |
129 | LoginButton(
130 | modifier = Modifier.height(100.dp)
131 | .aspectRatio(2f)
132 | .padding(24.dp),
133 | onClick = ::onLogin,
134 | isLoading = loadingState
135 | )
136 | }
137 | }
138 | }
139 |
140 |
141 | @Composable
142 | private fun AccountTextField(
143 | modifier: Modifier = Modifier,
144 | account: String,
145 | onAccountTextChange: (String) -> Unit,
146 | ) {
147 | var isError by remember { mutableStateOf(false) }
148 | OutlinedTextField(
149 | value = account,
150 | onValueChange = {
151 | isError = !it.matches("^[0-9]{0,15}$".toRegex())
152 | onAccountTextChange(it)
153 | },
154 | modifier = modifier,
155 | label = { Text(R.String.Login) },
156 | leadingIcon = { Icon(Icons.Default.AccountCircle, null) },
157 | isError = isError,
158 | keyboardOptions = KeyboardOptions(
159 | keyboardType = KeyboardType.Text,
160 | imeAction = ImeAction.Next
161 | ),
162 | singleLine = true
163 | )
164 | }
165 |
166 | @Composable
167 | private fun PasswordTextField(
168 | modifier: Modifier = Modifier,
169 | password: String,
170 | onPasswordTextChange: (String) -> Unit,
171 | ) {
172 | var passwordVisualTransformation: VisualTransformation by remember { mutableStateOf(PasswordVisualTransformation()) }
173 |
174 | OutlinedTextField(
175 | value = password,
176 | onValueChange = {
177 | onPasswordTextChange(it)
178 | },
179 | modifier = modifier,
180 | label = { Text(R.String.Password) },
181 | leadingIcon = {
182 | Icon(
183 | imageVector = Icons.Default.VpnKey,
184 | contentDescription = null
185 | )
186 | },
187 | trailingIcon = {
188 | IconButton(onClick = {
189 | passwordVisualTransformation =
190 | if (passwordVisualTransformation != VisualTransformation.None)
191 | VisualTransformation.None
192 | else
193 | PasswordVisualTransformation()
194 | }) {
195 | Icon(
196 | imageVector = Icons.Default.RemoveRedEye,
197 | contentDescription = null
198 | )
199 | }
200 | },
201 | visualTransformation = passwordVisualTransformation,
202 | keyboardOptions = KeyboardOptions(
203 | keyboardType = KeyboardType.Password,
204 | imeAction = ImeAction.Done
205 | ),
206 | singleLine = true
207 | )
208 | }
209 |
210 |
211 | @Composable
212 | private fun CheckProtocol(
213 | modifier: Modifier,
214 | protocol: BotConfiguration.MiraiProtocol,
215 | onProtocolChange: (BotConfiguration.MiraiProtocol) -> Unit
216 | ) {
217 | var expanded by remember { mutableStateOf(false) }
218 | val suggestions = enumValues()
219 |
220 | var textFieldSize by remember { mutableStateOf(Size.Zero) }
221 |
222 | val icon = if (expanded)
223 | Icons.Filled.ArrowDropUp
224 | else
225 | Icons.Filled.ArrowDropDown
226 |
227 | Column(modifier = modifier.padding(vertical = 5.dp)) {
228 | OutlinedTextField(
229 | value = protocol.name,
230 | onValueChange = { onProtocolChange(enumValueOf(it)) },
231 | modifier = Modifier
232 | .fillMaxWidth(fraction = 0.4f)
233 | .onGloballyPositioned { coordinates ->
234 | //This value is used to assign to the DropDown the same width
235 | textFieldSize = coordinates.size.toSize()
236 | },
237 | readOnly = true,
238 | trailingIcon = {
239 | Icon(icon, null,
240 | Modifier.clickable { expanded = !expanded })
241 | }
242 | )
243 | DropdownMenu(
244 | expanded = expanded,
245 | onDismissRequest = { expanded = false },
246 | modifier = Modifier
247 | .width(with(LocalDensity.current) { textFieldSize.width.toDp() })
248 | ) {
249 | suggestions.forEach { label ->
250 | DropdownMenuItem(onClick = {
251 | onProtocolChange(label)
252 | expanded = false
253 | }) {
254 | Text(text = label.name)
255 | }
256 | }
257 | }
258 | }
259 | }
260 |
261 | @Composable
262 | private fun LoginButton(
263 | modifier: Modifier = Modifier,
264 | onClick: () -> Unit,
265 | isLoading: Boolean
266 | ) = Button(
267 | onClick = onClick,
268 | modifier = modifier
269 | ) {
270 | if (isLoading)
271 | HorizontalDottedProgressBar()
272 | else
273 | Text("Login")
274 | }
275 |
276 | @Composable
277 | private fun HorizontalDottedProgressBar() {
278 | val color = MaterialTheme.colors.onPrimary
279 | val transition = rememberInfiniteTransition()
280 | val state by transition.animateFloat(
281 | initialValue = 0f,
282 | targetValue = 6f,
283 | animationSpec = infiniteRepeatable(
284 | animation = tween(
285 | durationMillis = 700,
286 | easing = LinearEasing
287 | ),
288 | repeatMode = RepeatMode.Reverse
289 | )
290 | )
291 |
292 | Canvas(
293 | modifier = Modifier
294 | .fillMaxWidth()
295 | .height(55.dp),
296 | ) {
297 |
298 | val radius = (4.dp).value
299 | val padding = (6.dp).value
300 |
301 | for (i in 1..5) {
302 | if (i - 1 == state.toInt()) {
303 | drawCircle(
304 | radius = radius * 2,
305 | brush = SolidColor(value = color),
306 | center = Offset(
307 | x = center.x + radius * 2 * (i - 3) + padding * (i - 3),
308 | y = center.y
309 | )
310 | )
311 | } else {
312 | drawCircle(
313 | radius = radius,
314 | brush = SolidColor(value = color),
315 | center = Offset(
316 | x = center.x + radius * 2 * (i - 3) + padding * (i - 3),
317 | y = center.y
318 | )
319 | )
320 | }
321 | }
322 | }
323 | }
324 |
325 | @Composable
326 | fun AsyncImage(
327 | load: suspend () -> T,
328 | painterFor: @Composable (T) -> Painter,
329 | contentDescription: String?,
330 | modifier: Modifier = Modifier,
331 | contentScale: ContentScale = ContentScale.Fit,
332 | ) {
333 | val image: T? by produceState(null) {
334 | value = withContext(Dispatchers.IO) {
335 | try {
336 | load()
337 | } catch (e: IOException) {
338 | e.printStackTrace()
339 | null
340 | }
341 | }
342 | }
343 |
344 | if (image != null) {
345 | Image(
346 | painter = painterFor(image!!),
347 | contentDescription = contentDescription,
348 | contentScale = contentScale,
349 | modifier = modifier
350 | )
351 | }
352 | }
353 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/login/LoginSolverContent.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.login
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.material.Button
6 | import androidx.compose.material.Scaffold
7 | import androidx.compose.material.Text
8 | import androidx.compose.material.TextField
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Alignment
13 |
14 |
15 | @Composable
16 | fun LoginSolverContent(
17 | title: String,
18 | tip: String,
19 | onFinish: (String) -> Unit,
20 | load: @Composable () -> Unit,
21 | refresh: () -> Unit,
22 | exit: () -> Unit
23 | ) {
24 | val (innerContent, setInnerContent) = remember(tip, title) { mutableStateOf("") }
25 | Scaffold(
26 | topBar = {
27 | Text(title)
28 | }
29 | ) {
30 | Column(
31 | horizontalAlignment = Alignment.CenterHorizontally
32 | ) {
33 | Text(tip)
34 | load()
35 | TextField(
36 | value = innerContent,
37 | onValueChange = setInnerContent
38 | )
39 | Row {
40 | Button({onFinish(innerContent)}) {
41 | Text("Finish")
42 | }
43 | Button(refresh) {
44 | Text("Refresh")
45 | }
46 |
47 | Button(exit) {
48 | Text("Exit")
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/message/BotMessage.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.message
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 |
6 | @Composable
7 | fun BotMessage() {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/message/Message.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.message
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | fun Message() {
8 | Column {
9 | // message.botList.forEach {
10 | // Text("目前消息流量:${it.messageSpeed}")
11 | // }
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/plugins/Annotation.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.plugins
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.material.Icon
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.vector.ImageVector
14 | import androidx.compose.ui.text.AnnotatedString
15 | import androidx.compose.ui.text.SpanStyle
16 | import androidx.compose.ui.text.buildAnnotatedString
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.text.style.TextOverflow
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import com.youngerhousea.mirai.compose.resource.R
22 | import net.mamoe.mirai.console.command.Command
23 | import net.mamoe.mirai.console.command.Command.Companion.allNames
24 | import net.mamoe.mirai.console.data.*
25 | import net.mamoe.mirai.console.plugin.*
26 | import net.mamoe.mirai.console.plugin.jvm.JavaPlugin
27 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
28 |
29 | internal inline val Plugin.annotatedName: AnnotatedString
30 | get() = buildAnnotatedString {
31 | pushStyle(SpanStyle(fontWeight = FontWeight.Medium, fontSize = 20.sp))
32 | append(name.ifEmpty { "Unknown" })
33 | pop()
34 | toAnnotatedString()
35 | }
36 |
37 | private inline val Plugin.annotatedAuthor: AnnotatedString
38 | get() = buildAnnotatedString {
39 | pushStyle(SpanStyle(fontSize = 13.sp))
40 | append("Author:")
41 | append(author.ifEmpty { "Unknown" })
42 | toAnnotatedString()
43 | }
44 |
45 | private inline val Plugin.annotatedInfo: AnnotatedString
46 | get() = buildAnnotatedString {
47 | pushStyle(SpanStyle(fontSize = 13.sp))
48 | append("Info:")
49 | append(info.ifEmpty { "Unknown" })
50 | toAnnotatedString()
51 | }
52 |
53 | private val Plugin.languageIcon: ImageVector
54 | get() =
55 | when (this) {
56 | is JavaPlugin -> {
57 | R.Image.Java
58 | }
59 | is KotlinPlugin -> {
60 | R.Image.Kotlin
61 | }
62 | else -> error("No icon current")
63 | }
64 |
65 | val Plugin.annotatedDescription: AnnotatedString
66 | get() = buildAnnotatedString {
67 | pushStyle(SpanStyle(fontWeight = FontWeight.Medium, color = Color.Black, fontSize = 20.sp))
68 | append("Name:${this@annotatedDescription.name.ifEmpty { "Unknown" }}\n")
69 | append("ID:${this@annotatedDescription.id}\n")
70 | append("Version:${this@annotatedDescription.version}\n")
71 | append("Info:${this@annotatedDescription.info.ifEmpty { "None" }}\n")
72 | append("Author:${this@annotatedDescription.author}\n")
73 | append("Dependencies:${this@annotatedDescription.dependencies}")
74 | pop()
75 | toAnnotatedString()
76 | }
77 |
78 | @Composable
79 | internal fun PluginDescription(plugin: Plugin, modifier: Modifier = Modifier) {
80 | Column(modifier = modifier) {
81 | Text(plugin.annotatedName, overflow = TextOverflow.Ellipsis, maxLines = 1)
82 | Spacer(Modifier.height(20.dp))
83 | Text(plugin.annotatedAuthor, overflow = TextOverflow.Ellipsis, maxLines = 1)
84 | Spacer(Modifier.height(10.dp))
85 | Text(plugin.annotatedInfo, overflow = TextOverflow.Ellipsis, maxLines = 1)
86 | Spacer(Modifier.height(10.dp))
87 | Row(verticalAlignment = Alignment.CenterVertically) {
88 | Icon(plugin.languageIcon, null)
89 | }
90 | }
91 | }
92 |
93 | internal inline val PluginData.annotatedExplain: AnnotatedString
94 | get() = buildAnnotatedString {
95 | pushStyle(SpanStyle(fontSize = 20.sp))
96 | append(
97 | when (this@annotatedExplain) {
98 | is AutoSavePluginConfig -> "自动保存配置"
99 | is ReadOnlyPluginConfig -> "只读配置"
100 | is AutoSavePluginData -> "自动保存数据"
101 | is ReadOnlyPluginData -> "只读数据"
102 | else -> "未知"
103 | }
104 | )
105 | append(':')
106 | append(this@annotatedExplain.saveName)
107 | toAnnotatedString()
108 | }
109 |
110 |
111 | internal inline val Command.simpleDescription: AnnotatedString
112 | get() = buildAnnotatedString {
113 | append(allNames.joinToString { " " })
114 | append('\n')
115 | append(usage)
116 | append('\n')
117 | append(description)
118 | toAnnotatedString()
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/plugins/PluginList.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.plugins
2 |
3 | import androidx.compose.desktop.ui.tooling.preview.Preview
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.grid.GridCells
7 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
8 | import androidx.compose.foundation.lazy.grid.items
9 | import androidx.compose.foundation.shape.CornerSize
10 | import androidx.compose.material.*
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.unit.dp
16 | import com.youngerhousea.mirai.compose.resource.R
17 | import net.mamoe.mirai.console.MiraiConsole
18 | import net.mamoe.mirai.console.permission.Permission
19 | import net.mamoe.mirai.console.permission.PermissionId
20 | import net.mamoe.mirai.console.plugin.Plugin
21 | import net.mamoe.mirai.console.plugin.loader.PluginLoader
22 | import java.awt.Desktop
23 | import kotlin.io.path.div
24 |
25 | @Composable
26 | fun PluginList(
27 | plugins: List,
28 | onPluginClick: (Plugin) -> Unit
29 | ) {
30 | val state = remember { SnackbarHostState() }
31 |
32 | Scaffold(
33 | scaffoldState = rememberScaffoldState(snackbarHostState = state),
34 | floatingActionButton = {
35 | FloatingActionButton(
36 | modifier = Modifier.padding(50.dp),
37 | backgroundColor = Color(0xff6EC177),
38 | contentColor = Color.White,
39 | onClick = {
40 | Desktop.getDesktop().open((MiraiConsole.rootPath / "plugins").toFile())
41 | },
42 | shape = MaterialTheme.shapes.medium.copy(CornerSize(percent = 50))
43 | ) {
44 | Icon(R.Icon.Add, R.String.Plugin.Add)
45 | }
46 | }
47 | ) {
48 | LazyVerticalGrid(
49 | columns = GridCells.Adaptive(300.dp),
50 | modifier = Modifier.fillMaxSize(),
51 | contentPadding = PaddingValues(20.dp)
52 | ) {
53 | items(plugins) { plugin ->
54 | Card(
55 | Modifier
56 | .padding(10.dp)
57 | .clickable(onClick = { onPluginClick(plugin) })
58 | .requiredHeight(150.dp)
59 | .fillMaxWidth(),
60 | backgroundColor = Color(0xff979595),
61 | contentColor = Color(0xffffffff)
62 | ) {
63 | PluginDescription(plugin, Modifier.padding(10.dp))
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | object FakePlugin : Plugin {
71 | override val isEnabled: Boolean = true
72 | override val loader: PluginLoader<*, *>
73 | get() = error("Not yet implemented")
74 | override val parentPermission: Permission
75 | get() = error("Not yet implemented")
76 |
77 | override fun permissionId(name: String): PermissionId {
78 | error("Not yet implemented")
79 | }
80 |
81 | }
82 |
83 | // TODO: Need more action to enable preview
84 | @Preview
85 | @Composable
86 | fun PluginPreview() {
87 | PluginList(listOf(FakePlugin, FakePlugin)) {}
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/plugins/Plugins.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.plugins
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.ui.window.AwtWindow
6 | import com.youngerhousea.mirai.compose.console.viewModel
7 | import com.youngerhousea.mirai.compose.viewmodel.Plugins
8 | import com.youngerhousea.mirai.compose.viewmodel.PluginsViewModel
9 | import net.mamoe.mirai.console.plugin.PluginManager
10 | import java.awt.FileDialog
11 | import java.awt.Frame
12 |
13 | @Composable
14 | fun Plugins(plugins: Plugins = viewModel { PluginsViewModel() }) {
15 | val state by plugins.state
16 |
17 | if(state.isFileChooserVisible) {
18 | FileDialog {
19 |
20 | }
21 | }
22 |
23 | when (val route = state.navigate) {
24 | Plugins.Route.List ->
25 | PluginList(
26 | plugins = PluginManager.plugins,
27 | onPluginClick = { plugins.dispatch(Plugins.Route.Single(it)) },
28 | )
29 | is Plugins.Route.Single ->
30 | SinglePlugin(
31 | plugin = route.plugin,
32 | onExit = { plugins.dispatch(Plugins.Route.List) }
33 | )
34 |
35 | }
36 | }
37 |
38 |
39 | @Composable
40 | private fun FileDialog(
41 | parent: Frame? = null,
42 | onCloseRequest: (result: String?) -> Unit
43 | ) = AwtWindow(
44 | create = {
45 | object : FileDialog(parent, "Choose a file", LOAD) {
46 | override fun setVisible(value: Boolean) {
47 | super.setVisible(value)
48 | if (value) {
49 | onCloseRequest(file)
50 | }
51 | }
52 | }
53 | },
54 | dispose = FileDialog::dispose
55 | )
56 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/plugins/SinglePlugin.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.plugins
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material.*
9 | import androidx.compose.runtime.*
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.text.style.TextAlign
14 | import androidx.compose.ui.unit.dp
15 | import com.youngerhousea.mirai.compose.console.impl.MiraiCompose
16 | import com.youngerhousea.mirai.compose.console.impl.get
17 | import com.youngerhousea.mirai.compose.resource.R
18 | import kotlinx.coroutines.launch
19 | import net.mamoe.mirai.console.command.Command
20 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registeredCommands
21 | import net.mamoe.mirai.console.data.PluginData
22 | import net.mamoe.mirai.console.plugin.Plugin
23 | import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin
24 | import net.mamoe.yamlkt.Yaml
25 | import kotlin.reflect.KProperty
26 |
27 | @Composable
28 | fun SinglePlugin(
29 | plugin: Plugin,
30 | onExit: () -> Unit
31 | ) {
32 | Scaffold(topBar = {
33 | TopAppBar(title = {
34 | Text(
35 | plugin.annotatedName,
36 | textAlign = TextAlign.Center,
37 | maxLines = 1,
38 | )
39 | }, navigationIcon = {
40 | Icon(
41 | R.Icon.Back,
42 | null,
43 | Modifier.clickable(onClick = onExit)
44 | )
45 | })
46 | }) {
47 | JvmPlugin(plugin as AbstractJvmPlugin)
48 | }
49 | }
50 |
51 | enum class PluginTab {
52 | Description,
53 | Data,
54 | Command
55 | }
56 |
57 | @Composable
58 | fun JvmPlugin(
59 | plugin: AbstractJvmPlugin,
60 | ) {
61 | Column {
62 | val (pluginTab, setPluginTab) = rememberSaveable(plugin) { mutableStateOf(PluginTab.Data) }
63 |
64 | TabRow(pluginTab.ordinal) {
65 | for (current in enumValues()) {
66 | Tab(pluginTab == current, onClick = {
67 | setPluginTab(current)
68 | }, content = {
69 | Text(current.name)
70 | })
71 | }
72 | }
73 |
74 | when (pluginTab) {
75 | PluginTab.Description -> PluginDescription(plugin)
76 | PluginTab.Data -> PluginDataList(MiraiCompose.configStorageForBuiltIns[plugin])
77 | PluginTab.Command -> PluginCommands(plugin.registeredCommands)
78 | }
79 | }
80 | }
81 |
82 | @Composable
83 | fun PluginDescription(plugin:Plugin) {
84 | Column(
85 | Modifier.fillMaxSize(),
86 | verticalArrangement = Arrangement.Center,
87 | horizontalAlignment = Alignment.CenterHorizontally
88 | ) {
89 | Text(plugin.annotatedDescription)
90 | }
91 | }
92 |
93 | @Composable
94 | fun PluginDataList(pluginDataList: List) {
95 | val scope = rememberCoroutineScope()
96 | val snackbarHostState = remember { SnackbarHostState() }
97 | Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
98 | LazyColumn(
99 | verticalArrangement = Arrangement.SpaceAround,
100 | horizontalAlignment = Alignment.CenterHorizontally
101 | ) {
102 | items(pluginDataList) { pluginData ->
103 | EditView(pluginData,
104 | onEditSuccess = {
105 | scope.launch {
106 | snackbarHostState.showSnackbar(R.String.EditSuccess)
107 | }
108 | },
109 | onEditFailure = {
110 | scope.launch {
111 | snackbarHostState.showSnackbar(R.String.EditFailure)
112 | }
113 | }
114 | )
115 | }
116 | }
117 | }
118 | }
119 |
120 |
121 | @Composable
122 | fun EditView(pluginData: PluginData, onEditSuccess: () -> Unit, onEditFailure: (Throwable) -> Unit) {
123 | var obValue by pluginData
124 |
125 | var textField by remember(pluginData) { mutableStateOf(obValue) }
126 |
127 | Row(
128 | Modifier.fillMaxWidth().padding(bottom = 40.dp),
129 | horizontalArrangement = Arrangement.SpaceEvenly,
130 | ) {
131 | OutlinedTextField(textField, {
132 | textField = it
133 | })
134 | Button(
135 | {
136 | runCatching {
137 | obValue = textField
138 | }.onSuccess {
139 | onEditSuccess()
140 | }.onFailure {
141 | textField = obValue
142 | onEditFailure(it)
143 | }
144 | },
145 | Modifier
146 | .requiredWidth(100.dp)
147 | .background(MaterialTheme.colors.background)
148 | ) {
149 | Text("Change")
150 | }
151 | }
152 |
153 | }
154 |
155 |
156 | operator fun PluginData.getValue(thisRef: Any?, property: KProperty<*>): String =
157 | Yaml.encodeToString(updaterSerializer, Unit)
158 |
159 | operator fun PluginData.setValue(thisRef: Any?, property: KProperty<*>, value: String) =
160 | Yaml.decodeFromString(updaterSerializer, value)
161 |
162 |
163 | @Composable
164 | fun PluginCommands(commands: List) {
165 | val snackbarHostState = remember { SnackbarHostState() }
166 |
167 | Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
168 | LazyColumn {
169 | items(commands) { registeredCommand ->
170 | Column {
171 | var commandValue by remember { mutableStateOf("") }
172 | Text(registeredCommand.simpleDescription)
173 | TextField(commandValue, {
174 | commandValue = it
175 | })
176 | Button({
177 | // scope.launch {
178 | // val result = ConsoleCommandSender.executeCommand(commandValue)
179 | // when (result) {
180 | // is CommandExecuteResult.Success -> {
181 | // }
182 | // }
183 | // }
184 | }) {
185 | Text("Quick go")
186 | }
187 | }
188 | Spacer(Modifier.height(20.dp))
189 | }
190 | }
191 | }
192 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/setting/AutoLogin.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.setting
2 | //
3 | //import androidx.compose.animation.AnimatedVisibility
4 | //import androidx.compose.animation.ExperimentalAnimationApi
5 | //import androidx.compose.desktop.ui.tooling.preview.Preview
6 | //import androidx.compose.foundation.clickable
7 | //import androidx.compose.foundation.layout.*
8 | //import androidx.compose.material.*
9 | //import androidx.compose.runtime.Composable
10 | //import androidx.compose.runtime.mutableStateListOf
11 | //import androidx.compose.runtime.remember
12 | //import androidx.compose.ui.Alignment
13 | //import androidx.compose.ui.Modifier
14 | //import androidx.compose.ui.unit.dp
15 | //import com.youngerhousea.mirai.compose.model.LoginCredential
16 | //import com.youngerhousea.mirai.compose.resource.R
17 | //import com.youngerhousea.mirai.compose.ui.EnumTabRowWithContent
18 |
19 | // IR ERROR need repair after
20 | //@OptIn(ExperimentalAnimationApi::class)
21 | //@Composable
22 | //fun AutoLoginSetting(
23 | // accounts: List,
24 | // onExit: () -> Unit,
25 | // onEditLoginCredential: (index: Int, loginCredential: LoginCredential) -> Unit,
26 | // onAddAutoLoginCredential: (LoginCredential) -> Unit
27 | //) {
28 | // Scaffold(
29 | // modifier = Modifier.fillMaxSize(),
30 | // topBar = {
31 | // Icon(
32 | // R.Icon.Back,
33 | // null,
34 | // Modifier.clickable(onClick = onExit)
35 | // )
36 | // }) {
37 | //
38 | // Column(
39 | // modifier = Modifier.fillMaxSize(),
40 | // horizontalAlignment = Alignment.CenterHorizontally,
41 | // verticalArrangement = Arrangement.SpaceBetween
42 | // ) {
43 | // Text("Now Accounts")
44 | // AnimatedVisibility(accounts.isEmpty()) {
45 | // Text("Not have Auto Login Accounts")
46 | // }
47 | // AnimatedVisibility(accounts.isNotEmpty()) {
48 | // accounts.forEachIndexed { index, loginCredential ->
49 | // AutoLoginPage(loginCredential) {
50 | // onEditLoginCredential(index, loginCredential)
51 | // }
52 | // }
53 | // }
54 | //
55 | // Button({
56 | // onAddAutoLoginCredential(LoginCredential())
57 | // }, modifier = Modifier.align(Alignment.End)) {
58 | // Text("Create a auto login account")
59 | // }
60 | // }
61 | // }
62 | //}
63 | //
64 | //@Composable
65 | //private inline fun AutoLoginPage(
66 | // loginCredential: LoginCredential,
67 | // modifier: Modifier = Modifier,
68 | // crossinline onSubmit: (loginCredential: LoginCredential) -> Unit
69 | //) {
70 | // with(loginCredential) {
71 | // Column(modifier.fillMaxSize().padding(horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
72 | // Row(horizontalArrangement = Arrangement.SpaceEvenly) {
73 | // TextField(account, onValueChange = {
74 | // onSubmit(loginCredential.copy(account = it))
75 | // })
76 | //
77 | // TextField(password, onValueChange = {
78 | // onSubmit(loginCredential.copy(password = it))
79 | // })
80 | // }
81 | //
82 | // EnumTabRowWithContent(passwordKind, onClick = {
83 | // onSubmit(loginCredential.copy(passwordKind = it))
84 | // }) {
85 | // Text(it.name)
86 | // }
87 | //
88 | // EnumTabRowWithContent(protocolKind, onClick = {
89 | // onSubmit(loginCredential.copy(protocolKind = it))
90 | // }) {
91 | // Text(it.name)
92 | // }
93 | // }
94 | //
95 | // }
96 | //}
97 |
98 | //@Composable
99 | //@Preview
100 | //fun AutoLoginSettingPreview() {
101 | // val loginCredentials = remember { mutableStateListOf() }
102 | // AutoLoginSetting(
103 | // loginCredentials,
104 | // onExit = {},
105 | // onEditLoginCredential = { index, loginCredential ->
106 | // loginCredentials[index] = loginCredential
107 | // },
108 | // onAddAutoLoginCredential = loginCredentials::add
109 | // )
110 | //}
--------------------------------------------------------------------------------
/app/src/main/kotlin/ui/setting/Setting.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.ui.setting
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | fun Setting() {
8 | Text("Nothing")
9 | }
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/viewmodel/ConsoleLogViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.viewmodel
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import com.youngerhousea.mirai.compose.console.ViewModelScope
7 | import kotlinx.coroutines.launch
8 | import net.mamoe.mirai.console.MiraiConsole
9 | import net.mamoe.mirai.console.command.*
10 | import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter
11 | import net.mamoe.mirai.console.command.descriptor.CommandReceiverParameter
12 | import net.mamoe.mirai.console.command.descriptor.CommandValueParameter
13 | import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
14 | import net.mamoe.mirai.console.command.parse.CommandCall
15 | import net.mamoe.mirai.console.command.parse.CommandValueArgument
16 | import net.mamoe.mirai.console.util.ConsoleInternalApi
17 | import net.mamoe.mirai.console.util.cast
18 | import net.mamoe.mirai.console.util.safeCast
19 | import net.mamoe.mirai.utils.MiraiLogger
20 | import net.mamoe.mirai.utils.warning
21 | import kotlin.reflect.KClass
22 | import kotlin.reflect.full.isSubclassOf
23 |
24 |
25 | /**
26 | * Compose的所有日志
27 | *
28 | */
29 | interface ConsoleLog {
30 |
31 | val state: State
32 |
33 | fun dispatch(action: Action)
34 |
35 | sealed interface Action {
36 | class UpdateSearchContent(val content: String) : Action
37 | class UpdateCurrentCommand(val content: String) : Action
38 | object EnterCommand : Action
39 | object SetSearchBar : Action
40 | }
41 |
42 | data class InnerState(
43 | val searchContent: String = "",
44 | val currentCommand: String = "",
45 | val searchBarVisible: Boolean = false
46 | )
47 | }
48 |
49 |
50 |
51 | class ConsoleLogViewModel @OptIn(ConsoleInternalApi::class) constructor(
52 | private val logger: MiraiLogger = MiraiConsole.mainLogger,
53 | ) : ConsoleLog, ViewModelScope() {
54 | override val state: MutableState = mutableStateOf(ConsoleLog.InnerState())
55 |
56 | override fun dispatch(action: ConsoleLog.Action) {
57 | viewModelScope.launch {
58 | state.value = reduce(state.value, action)
59 | }
60 | }
61 |
62 | private suspend fun reduce(value: ConsoleLog.InnerState, action: ConsoleLog.Action): ConsoleLog.InnerState {
63 | return when (action) {
64 | is ConsoleLog.Action.UpdateSearchContent -> value.copy(searchContent = action.content)
65 | is ConsoleLog.Action.UpdateCurrentCommand -> value.copy(currentCommand = action.content)
66 | is ConsoleLog.Action.EnterCommand -> {
67 | SolveCommandResult(value.currentCommand, logger)
68 | return value.copy(currentCommand = "")
69 | }
70 | is ConsoleLog.Action.SetSearchBar -> value.copy(searchBarVisible = !value.searchBarVisible)
71 | }
72 | }
73 |
74 | }
75 |
76 | //@OptIn(ExperimentalCommandDescriptors::class)
77 | //private suspend fun CommandPrompt(
78 | // currentCommand: String,
79 | //): String {
80 | // return when (val result = ConsoleCommandSender.executeCommand(currentCommand)) {
81 | // is CommandExecuteResult.UnmatchedSignature -> {
82 | // result.failureReasons.render(result.command, result.call)
83 | // }
84 | // else -> ""
85 | // }
86 | //}
87 |
88 | @OptIn(ExperimentalCommandDescriptors::class)
89 | private suspend fun SolveCommandResult(
90 | currentCommand: String,
91 | logger: MiraiLogger
92 | ) {
93 | when (val result = ConsoleCommandSender.executeCommand(currentCommand)) {
94 | is CommandExecuteResult.Success -> {
95 | }
96 | is CommandExecuteResult.IllegalArgument -> {
97 | val message = result.exception.message
98 | if (message != null) {
99 | logger.warning(message)
100 | } else logger.warning(result.exception)
101 | }
102 | is CommandExecuteResult.ExecutionFailed -> {
103 | logger.error(result.exception)
104 | }
105 | is CommandExecuteResult.UnresolvedCommand -> {
106 | logger.warning { "未知指令: ${currentCommand}, 输入 /help 获取帮助" }
107 | }
108 | is CommandExecuteResult.PermissionDenied -> {
109 | logger.warning { "权限不足." }
110 | }
111 | is CommandExecuteResult.UnmatchedSignature -> {
112 | logger.warning { "参数不匹配, 你是否想执行: \n" + result.failureReasons.render(result.command, result.call) }
113 | }
114 | is CommandExecuteResult.Failure -> {
115 | logger.warning { result.toString() }
116 | }
117 | }
118 | }
119 |
120 |
121 | @OptIn(ExperimentalCommandDescriptors::class)
122 | private fun List.render(command: Command, call: CommandCall): String {
123 | val list =
124 | this.filter lambda@{ signature ->
125 | if (signature.failureReason.safeCast()?.parameter is AbstractCommandValueParameter.StringConstant) return@lambda false
126 | if (signature.signature.valueParameters.anyStringConstantUnmatched(call.valueArguments)) return@lambda false
127 | true
128 | }
129 | if (list.isEmpty()) {
130 | return command.usage
131 | }
132 | return list.joinToString("\n") { it.render(command) }
133 | }
134 |
135 | @OptIn(ExperimentalCommandDescriptors::class)
136 | private fun List>.anyStringConstantUnmatched(arguments: List): Boolean {
137 | return this.zip(arguments).any { (parameter, argument) ->
138 | parameter is AbstractCommandValueParameter.StringConstant && !parameter.accepts(argument, null)
139 | }
140 | }
141 |
142 | @OptIn(ExperimentalCommandDescriptors::class)
143 | internal fun UnmatchedCommandSignature.render(command: Command): String {
144 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
145 | val usage =
146 | net.mamoe.mirai.console.internal.command.CommandReflector.generateUsage(
147 | command,
148 | null,
149 | listOf(this.signature)
150 | )
151 | return usage.trim() + " (${failureReason.render()})"
152 | }
153 |
154 | @OptIn(ExperimentalCommandDescriptors::class)
155 | internal fun FailureReason.render(): String {
156 | return when (this) {
157 | is FailureReason.InapplicableArgument -> "参数类型错误"
158 | is FailureReason.InapplicableReceiverArgument -> "需要由 ${this.parameter.renderAsName()} 执行"
159 | is FailureReason.TooManyArguments -> "参数过多"
160 | is FailureReason.NotEnoughArguments -> "参数不足"
161 | is FailureReason.ResolutionAmbiguity -> "调用歧义"
162 | is FailureReason.ArgumentLengthMismatch -> {
163 | // should not happen, render it anyway.
164 | "参数长度不匹配"
165 | }
166 | }
167 | }
168 |
169 | @OptIn(ExperimentalCommandDescriptors::class)
170 | internal fun CommandReceiverParameter<*>.renderAsName(): String {
171 | val classifier = this.type.classifier.cast>()
172 | return when {
173 | classifier.isSubclassOf(ConsoleCommandSender::class) -> "控制台"
174 | classifier.isSubclassOf(FriendCommandSenderOnMessage::class) -> "好友私聊"
175 | classifier.isSubclassOf(FriendCommandSender::class) -> "好友"
176 | classifier.isSubclassOf(MemberCommandSenderOnMessage::class) -> "群内发言"
177 | classifier.isSubclassOf(MemberCommandSender::class) -> "群成员"
178 | classifier.isSubclassOf(GroupTempCommandSenderOnMessage::class) -> "群临时会话"
179 | classifier.isSubclassOf(GroupTempCommandSender::class) -> "群临时好友"
180 | classifier.isSubclassOf(UserCommandSender::class) -> "用户"
181 | else -> classifier.simpleName ?: classifier.toString()
182 | }
183 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/viewmodel/HostViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.viewmodel
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import com.youngerhousea.mirai.compose.console.ViewModelScope
7 | import kotlinx.coroutines.launch
8 | import net.mamoe.mirai.Bot
9 | import net.mamoe.mirai.event.GlobalEventChannel
10 | import net.mamoe.mirai.event.events.BotOnlineEvent
11 |
12 |
13 | interface Host {
14 | val state: State
15 |
16 | fun dispatch(event: Action)
17 |
18 | sealed interface Route {
19 | class BotMessage(val bot: Bot) : Route, Action
20 | object Message : Route, Action
21 | object Setting : Route, Action
22 | object About : Route, Action
23 | object Plugins : Route, Action
24 | object ConsoleLog : Route, Action
25 | }
26 |
27 | @Immutable
28 | data class InnerState(
29 | val currentBot: Bot? = null,
30 | val botList: List = listOf(),
31 | val menuExpand: Boolean = false,
32 | val navigate: Route = Route.Message,
33 | val dialogExpand: Boolean = false
34 | )
35 |
36 | sealed interface Action {
37 | object OpenMenu : Action
38 | object CloseMenu : Action
39 | object OpenLoginDialog : Action
40 | object CloseLoginDialog : Action
41 | }
42 | }
43 |
44 |
45 | class HostViewModel : ViewModelScope(), Host {
46 | override val state = mutableStateOf(Host.InnerState())
47 |
48 | override fun dispatch(event: Host.Action) {
49 | state.value = reduce(state.value, event)
50 | }
51 |
52 | private fun reduce(state: Host.InnerState, action: Host.Action): Host.InnerState {
53 | return when (action) {
54 | Host.Action.CloseLoginDialog -> state.copy(dialogExpand = false)
55 | Host.Action.OpenLoginDialog -> state.copy(dialogExpand = true)
56 | is Host.Action.OpenMenu -> state.copy(menuExpand = true)
57 | is Host.Action.CloseMenu -> state.copy(menuExpand = false)
58 | is Host.Route.About -> state.copy(navigate = action)
59 | is Host.Route.Message -> state.copy(navigate = action)
60 | is Host.Route.Plugins -> state.copy(navigate = action)
61 | is Host.Route.Setting -> state.copy(navigate = action)
62 | is Host.Route.BotMessage -> state.copy(currentBot = action.bot)
63 | is Host.Route.ConsoleLog -> state.copy(navigate = action)
64 | }
65 | }
66 |
67 | init {
68 | viewModelScope.launch {
69 | GlobalEventChannel.subscribeAlways { event ->
70 | state.value =
71 | state.value.copy(botList = state.value.botList + event.bot, currentBot = event.bot)
72 | }
73 | }
74 | }
75 |
76 | }
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/viewmodel/PluginsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.viewmodel
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import com.youngerhousea.mirai.compose.console.ViewModelScope
6 | import kotlinx.coroutines.launch
7 | import net.mamoe.mirai.console.plugin.Plugin
8 |
9 | interface Plugins {
10 |
11 | val state: State
12 |
13 | fun dispatch(action: Action)
14 |
15 | sealed interface Route {
16 | object List : Route, Action
17 | class Single(val plugin: Plugin) : Route, Action
18 | }
19 |
20 | sealed interface Action {
21 | class LoadingPlugin(val pluginName: String) : Action
22 | object OpenFileChooser : Action
23 | }
24 |
25 |
26 | data class InnerState(
27 | val navigate: Route = Route.List,
28 | val isFileChooserVisible: Boolean = false
29 | )
30 | }
31 |
32 | class PluginsViewModel : Plugins, ViewModelScope() {
33 | override val state = mutableStateOf(Plugins.InnerState())
34 |
35 | override fun dispatch(action: Plugins.Action) {
36 | viewModelScope.launch {
37 | state.value = reduce(action, state.value)
38 | }
39 | }
40 |
41 | private fun reduce(action: Plugins.Action, state: Plugins.InnerState): Plugins.InnerState {
42 | return when (action) {
43 | is Plugins.Route.List -> state.copy(navigate = action)
44 | is Plugins.Route.Single -> state.copy(navigate = action)
45 | is Plugins.Action.LoadingPlugin -> state
46 | is Plugins.Action.OpenFileChooser -> state.copy(isFileChooserVisible = true)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_close.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_java.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_kotlin.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_max.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_min.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/resources/ic_mirai.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/resources/mirai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sonder-joker/mirai-compose/9c3238171bec75f6f835fc18ded1a4f807d0f681/app/src/main/resources/mirai.png
--------------------------------------------------------------------------------
/app/src/test/kotlin/Entrypoint.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose
2 |
3 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
4 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.load
5 | import org.junit.Test
6 |
7 | class EntrypointTest{
8 |
9 | @Test
10 | fun start() {
11 | startApplication {
12 | TestPlugin.load()
13 | TestPlugin.enable()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/TestPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose
2 |
3 | import mirai_compose.app.BuildConfig
4 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
5 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
6 |
7 | object TestPlugin: KotlinPlugin(
8 | description = JvmPluginDescription(BuildConfig.projectGroup,BuildConfig.projectVersion, BuildConfig.projectName) {
9 | author("测试1")
10 | info("测试2")
11 | }
12 | ) {
13 | override fun onEnable() {
14 | logger.info("Link start!")
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/console/FluentMVITest.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import org.junit.Test
5 |
6 | sealed class TestState {
7 | object Nothing : TestState()
8 | class A(
9 | val string: String
10 | ) : TestState()
11 |
12 | class B(
13 | val int: Int
14 | ) : TestState()
15 |
16 | class C(
17 | val long: Long
18 | ) : TestState()
19 | }
20 |
21 | sealed class TestEvent {
22 | object Empty:TestEvent()
23 | class AToB(val int:Int):TestEvent()
24 | class BToC(val long: Long):TestEvent()
25 | class CToA(val string:String):TestEvent()
26 |
27 | }
28 |
29 | class FluentMVITest {
30 | val testState = mutableStateOf(TestState.Nothing)
31 | val testEvent = TestEvent.Empty
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/console/MiraiComposeImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console
2 |
3 | import androidx.compose.ui.test.junit4.createComposeRule
4 | import org.junit.Rule
5 |
6 | internal class MiraiComposeImplTest {
7 |
8 | @get:Rule
9 | val composeTestRule = createComposeRule()
10 |
11 | @org.junit.Test
12 | fun `start mirai compose`() {
13 |
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/console/ViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.youngerhousea.mirai.compose.console
2 |
3 | import com.youngerhousea.mirai.compose.console.impl.ViewModelStoreImpl
4 | import org.junit.Before
5 | import kotlin.test.*
6 |
7 |
8 | internal class TestViewModel : ViewModel {
9 | var isDestroy = false
10 |
11 | override fun onDestroy() {
12 | isDestroy = true
13 | }
14 | }
15 |
16 | internal class ViewModelTest {
17 | private lateinit var viewModelStore: ViewModelStore
18 |
19 | @Before
20 | fun init() {
21 | viewModelStore = ViewModelStoreImpl()
22 | }
23 |
24 | @Test
25 | fun `viewModel function test`() {
26 | assertNull(viewModelStore.get(), "Before create ViewModel shouldn't exist")
27 |
28 | val viewModel = viewModelStore.getOrCreate { TestViewModel() }
29 | assertNotNull(viewModelStore.get(), "ViewModel shouldn't be null")
30 |
31 | viewModelStore.clean()
32 | assertTrue(viewModel.isDestroy, "ViewModel should already destroy")
33 | }
34 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.*
2 | import org.jetbrains.kotlin.utils.addToStdlib.safeAs
3 |
4 | group = MiraiCompose.group
5 | version = MiraiCompose.version
6 |
7 | allprojects {
8 | repositories {
9 | mavenCentral()
10 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
11 | google()
12 | }
13 | afterEvaluate {
14 | configureEncoding()
15 | configureKotlinExperimentalUsages()
16 | configureKotlinCompilerSettings()
17 | }
18 | }
19 |
20 |
21 | val experimentalAnnotations = arrayOf(
22 | "kotlin.Experimental",
23 | "kotlin.RequiresOptIn",
24 | "kotlin.ExperimentalUnsignedTypes",
25 | "kotlin.ExperimentalStdlibApi",
26 | "kotlin.experimental.ExperimentalTypeInference",
27 | )
28 |
29 |
30 | fun Project.configureEncoding() {
31 | tasks.withType() {
32 | options.encoding = "UTF8"
33 | }
34 | }
35 |
36 | fun Project.configureKotlinExperimentalUsages() {
37 | val sourceSets = kotlinSourceSets ?: return
38 |
39 | for (target in sourceSets)
40 | target.languageSettings.run {
41 | progressiveMode = true
42 | experimentalAnnotations.forEach { a ->
43 | optIn(a)
44 | }
45 | }
46 | }
47 |
48 | fun Project.configureKotlinCompilerSettings() {
49 | val kotlinCompilations = kotlinCompilations ?: return
50 | for (kotlinCompilation in kotlinCompilations) with(kotlinCompilation) {
51 | if (isKotlinJvmProject) {
52 | @Suppress("UNCHECKED_CAST")
53 | this as org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
54 | }
55 | kotlinOptions.freeCompilerArgs += "-Xjvm-default=all"
56 | }
57 | }
58 |
59 | val Project.kotlinSourceSets get() = extensions.findByName("kotlin").safeAs()?.sourceSets
60 |
61 | val Project.isKotlinJvmProject: Boolean get() = extensions.findByName("kotlin") is KotlinJvmProjectExtension
62 |
63 |
64 | val Project.kotlinTargets
65 | get() =
66 | extensions.findByName("kotlin").safeAs()?.target?.let { listOf(it) }
67 | ?: extensions.findByName("kotlin").safeAs()?.targets
68 |
69 | val Project.kotlinCompilations
70 | get() = kotlinTargets?.flatMap { it.compilations }
71 |
72 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | gradlePluginPortal()
7 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
8 | }
9 |
10 | dependencies {
11 | implementation(libs.plugin.serialization)
12 | implementation(libs.plugin.composejb)
13 | implementation(libs.plugin.jvm)
14 | implementation(libs.plugin.buildconfig)
15 | // implementation(libs.plugin.miraiconsole)
16 |
17 | }
18 |
19 | kotlin {
20 | sourceSets.all {
21 | languageSettings.optIn("kotlin.Experimental")
22 | languageSettings.optIn("kotlin.RequiresOptIn")
23 | }
24 | }
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/buildSrc/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("VERSION_CATALOGS")
2 |
3 | dependencyResolutionManagement {
4 | @Suppress("UnstableApiUsage")
5 | versionCatalogs {
6 | create("libs") {
7 | from(files("../gradle/libs.versions.toml"))
8 | }
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/MiraiCompose.kt:
--------------------------------------------------------------------------------
1 | object MiraiCompose {
2 | const val name = "mirai-compose"
3 | const val group = "com.youngerhousea.mirai"
4 | const val version = "1.1.5"
5 | const val mainClass = "com.youngerhousea.mirai.compose.EntryPointKt"
6 | const val windowsUUID = "01BBD7BE-A84F-314A-FA84-67B63728A416"
7 | }
--------------------------------------------------------------------------------
/docs/FEATURES.md:
--------------------------------------------------------------------------------
1 | # 如何使用
2 | 在项目主页右侧可以看到``Release``
3 | 
4 | 在右侧的``Release``处下载。
5 | 
6 | 使用相应格式安装。
7 |
8 | ## Windows
9 | 
10 | 双击exe可直接启动,另外,第一次启动可能需要使用管理员权限。
11 | 添加插件和自动登录都可见[mirai-console的文档](https://github.com/mamoe/mirai/tree/dev/mirai-console)
12 | 其它系统类似。
13 |
14 |
--------------------------------------------------------------------------------
/docs/design.md:
--------------------------------------------------------------------------------
1 | #对MiraiConsole的扩展
2 |
3 | 1.ViewModelOwner
4 |
5 | 2.Lifecycle
6 |
7 | 3.MiraiCompose
8 |
9 | ...
10 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "1.6.21"
3 | compose-jb = "1.2.0-alpha01-dev731"
4 | buildconfig = "3.0.2"
5 | mirai = "2.11.1"
6 | yamlkt = "0.10.2"
7 | json = "1.3.2"
8 | junit = "4.13"
9 |
10 | [libraries]
11 | plugin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
12 | plugin-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
13 | plugin-composejb = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version.ref = "compose-jb" }
14 | plugin-buildconfig = { group = "com.github.gmazzo", name = "gradle-buildconfig-plugin", version.ref = "buildconfig" }
15 |
16 | mirai-core = { group = "net.mamoe", name = "mirai-core", version.ref = "mirai" }
17 | mirai-console = { group = "net.mamoe", name = "mirai-console", version.ref = "mirai" }
18 | serialization-yaml = { group = "net.mamoe.yamlkt", name = "yamlkt", version.ref = "yamlkt" }
19 | serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "json" }
20 | junit4 = { group = "junit", name = "junit", version.ref = "junit" }
21 |
22 | kotlin-coroutinues-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version = "1.5.0" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sonder-joker/mirai-compose/9c3238171bec75f6f835fc18ded1a4f807d0f681/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
5 | # Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link.
6 | #
7 | # https://github.com/mamoe/mirai/blob/master/LICENSE
8 | #
9 |
10 | ##############################################################################
11 | ##
12 | ## Gradle start up script for UN*X
13 | ##
14 | ##############################################################################
15 |
16 | # Attempt to set APP_HOME
17 | # Resolve links: $0 may be a link
18 | PRG="$0"
19 | # Need this for relative symlinks.
20 | while [ -h "$PRG" ] ; do
21 | ls=`ls -ld "$PRG"`
22 | link=`expr "$ls" : '.*-> \(.*\)$'`
23 | if expr "$link" : '/.*' > /dev/null; then
24 | PRG="$link"
25 | else
26 | PRG=`dirname "$PRG"`"/$link"
27 | fi
28 | done
29 | SAVED="`pwd`"
30 | cd "`dirname \"$PRG\"`/" >/dev/null
31 | APP_HOME="`pwd -P`"
32 | cd "$SAVED" >/dev/null
33 |
34 | APP_NAME="Gradle"
35 | APP_BASE_NAME=`basename "$0"`
36 |
37 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
38 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
39 |
40 | # Use the maximum available, or set MAX_FD != -1 to use that value.
41 | MAX_FD="maximum"
42 |
43 | warn () {
44 | echo "$*"
45 | }
46 |
47 | die () {
48 | echo
49 | echo "$*"
50 | echo
51 | exit 1
52 | }
53 |
54 | # OS specific support (must be 'true' or 'false').
55 | cygwin=false
56 | msys=false
57 | darwin=false
58 | nonstop=false
59 | case "`uname`" in
60 | CYGWIN* )
61 | cygwin=true
62 | ;;
63 | Darwin* )
64 | darwin=true
65 | ;;
66 | MINGW* )
67 | msys=true
68 | ;;
69 | NONSTOP* )
70 | nonstop=true
71 | ;;
72 | esac
73 |
74 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
75 |
76 |
77 | # Determine the Java command to use to start the JVM.
78 | if [ -n "$JAVA_HOME" ] ; then
79 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
80 | # IBM's JDK on AIX uses strange locations for the executables
81 | JAVACMD="$JAVA_HOME/jre/sh/java"
82 | else
83 | JAVACMD="$JAVA_HOME/bin/java"
84 | fi
85 | if [ ! -x "$JAVACMD" ] ; then
86 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 | else
92 | JAVACMD="java"
93 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
94 |
95 | Please set the JAVA_HOME variable in your environment to match the
96 | location of your Java installation."
97 | fi
98 |
99 | # Increase the maximum file descriptors if we can.
100 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
101 | MAX_FD_LIMIT=`ulimit -H -n`
102 | if [ $? -eq 0 ] ; then
103 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
104 | MAX_FD="$MAX_FD_LIMIT"
105 | fi
106 | ulimit -n $MAX_FD
107 | if [ $? -ne 0 ] ; then
108 | warn "Could not set maximum file descriptor limit: $MAX_FD"
109 | fi
110 | else
111 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
112 | fi
113 | fi
114 |
115 | # For Darwin, add options to specify how the application appears in the dock
116 | if $darwin; then
117 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
118 | fi
119 |
120 | # For Cygwin or MSYS, switch paths to Windows format before running java
121 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
122 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
123 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
124 |
125 | JAVACMD=`cygpath --unix "$JAVACMD"`
126 |
127 | # We build the pattern for arguments to be converted via cygpath
128 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
129 | SEP=""
130 | for dir in $ROOTDIRSRAW ; do
131 | ROOTDIRS="$ROOTDIRS$SEP$dir"
132 | SEP="|"
133 | done
134 | OURCYGPATTERN="(^($ROOTDIRS))"
135 | # Add a user-defined pattern to the cygpath arguments
136 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
137 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
138 | fi
139 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
140 | i=0
141 | for arg in "$@" ; do
142 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
143 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
144 |
145 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
146 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
147 | else
148 | eval `echo args$i`="\"$arg\""
149 | fi
150 | i=`expr $i + 1`
151 | done
152 | case $i in
153 | 0) set -- ;;
154 | 1) set -- "$args0" ;;
155 | 2) set -- "$args0" "$args1" ;;
156 | 3) set -- "$args0" "$args1" "$args2" ;;
157 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
158 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
159 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
160 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
161 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
162 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
163 | esac
164 | fi
165 |
166 | # Escape application args
167 | save () {
168 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
169 | echo " "
170 | }
171 | APP_ARGS=`save "$@"`
172 |
173 | # Collect all arguments for the java command, following the shell quoting and substitution rules
174 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
175 |
176 | exec "$JAVACMD" "$@"
177 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "mirai-compose"
2 |
3 | include(":app")
4 |
5 | enableFeaturePreview("VERSION_CATALOGS")
6 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
7 |
8 |
--------------------------------------------------------------------------------