├── .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 |
2 | logo
3 | 4 | 5 | title 6 | 7 | ---- 8 | Mirai 是一个在全平台下运行,提供 QQ 协议支持的高效率机器人库 9 | 10 | 这个项目的名字来源于 11 |

京都动画作品《境界的彼方》栗山未来(Kuriyama Mirai)

12 |

CRYPTON初音未来为代表的创作与活动(Magical Mirai)

13 | 图标以及形象由画师DazeCake绘制 14 |
15 | 16 | # mirai-compose 17 | 18 | [![GitHub release (latest SemVer including pre-release)](https://img.shields.io/github/v/release/sonder-joker/mirai-compose?include_prereleases)](https://github.com/sonder-joker/mirai-compose/releases) 19 | ![QQ Group](https://img.shields.io/badge/交流群-1004268447-informational?style=flat-square&logo=tencent-qq) 20 | [![MiraiForum](https://img.shields.io/badge/官方论坛-mirai--forum-blueviolet?style=flat-square&logo=appveyor)](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 | ![image](https://user-images.githubusercontent.com/56215747/147228571-d775c5f1-e5a9-4172-b528-5ca86eb68dd2.png) 4 | 在右侧的``Release``处下载。 5 | ![image](https://user-images.githubusercontent.com/56215747/147228775-0dcf28ef-a408-46cf-8b9e-89e93936e227.png) 6 | 使用相应格式安装。 7 | 8 | ## Windows 9 | ![image](https://user-images.githubusercontent.com/56215747/147229017-08033a71-e334-4994-9dcb-dac2ca25bb75.png) 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 | --------------------------------------------------------------------------------