diff --git a/.idea/artifacts/adbpad_jvm_1_5_2.xml b/.idea/artifacts/adbpad_jvm_1_5_2.xml new file mode 100644 index 00000000..e17f3913 --- /dev/null +++ b/.idea/artifacts/adbpad_jvm_1_5_2.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml new file mode 100644 index 00000000..e8bd90cf --- /dev/null +++ b/.idea/ktlint-plugin.xml @@ -0,0 +1,7 @@ + + + + DISTRACT_FREE + DEFAULT + + \ No newline at end of file diff --git a/README.md b/README.md index 1d014227..8add7c54 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ https://github.com/user-attachments/assets/b250bf2b-e61c-4a6d-9872-6d3dcb3339e6 # ⬇️ Install -- Download from [here](https://github.com/kaleidot725/AdbPad/releases/tag/v1.5.2). +- Download from [here](https://github.com/kaleidot725/AdbPad/releases/tag/v2.0.0). - Setup adb path on Setting. https://github.com/user-attachments/assets/f5542ace-118d-4165-a138-49d59c7bda8b diff --git a/build.gradle.kts b/build.gradle.kts index dea08e49..114d80c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ plugins { } group = "jp.kaleidot725" -version = "1.5.2" +version = "2.0.0" kotlin { jvm() @@ -39,8 +39,6 @@ kotlin { implementation(libs.ktor.client.okhttp) implementation(libs.jSystemThemeDetectorVer) implementation(libs.coil) - implementation(libs.hot.reload.core) - implementation(libs.hot.reload.analysis) } sourceSets.jvmTest.dependencies { implementation(libs.junit5) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b0d500b9..2f51b7f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,8 @@ [versions] kotlin="2.1.20-Beta2" -compose_reload_kotlin = "2.1.20-firework.34" kotlin_coroutines="1.10.1" kotlin_serialization="1.8.0" -compose_hot_reload = "1.0.0-dev-39" +compose_hot_reload = "1.0.0-alpha01" compose="1.7.3" ktlint_plugin="12.1.2" adam="0.5.10" @@ -26,14 +25,12 @@ ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } lucide = { module = "com.composables:icons-lucide", version.ref = "lucide" } coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -hot-reload-core = { module = "org.jetbrains.compose:hot-reload-core", version.ref = "compose_hot_reload" } -hot-reload-analysis = { module = "org.jetbrains.compose:hot-reload-analysis", version.ref = "compose_hot_reload" } [plugins] -multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "compose_reload_kotlin" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "compose_reload_kotlin" } -compose-hot-reload = { id = "org.jetbrains.compose-hot-reload", version.ref = "compose_hot_reload" } +multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +compose-hot-reload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose_hot_reload" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } -kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "compose_reload_kotlin" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint_plugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 579e5185..8af855f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,8 +3,6 @@ pluginManagement { google() gradlePluginPortal() mavenCentral() - maven(file("../..//build/repo")) - maven("https://packages.jetbrains.team/maven/p/firework/dev") } } @@ -13,8 +11,6 @@ dependencyResolutionManagement { google() gradlePluginPortal() mavenCentral() - maven(file("../..//build/repo")) - maven("https://packages.jetbrains.team/maven/p/firework/dev") maven("https://jitpack.io") } } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/Main.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/Main.kt index 4081f5f9..e648c6e6 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/Main.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/Main.kt @@ -30,6 +30,7 @@ import jp.kaleidot725.adbpad.ui.di.stateHolderModule import jp.kaleidot725.adbpad.ui.screen.CommandScreen import jp.kaleidot725.adbpad.ui.screen.ScreenLayout import jp.kaleidot725.adbpad.ui.screen.error.AdbErrorScreen +import jp.kaleidot725.adbpad.ui.screen.screenshot.ScreenshotAction import jp.kaleidot725.adbpad.ui.screen.screenshot.ScreenshotScreen import jp.kaleidot725.adbpad.ui.screen.setting.SettingScreen import jp.kaleidot725.adbpad.ui.screen.setting.SettingStateHolder @@ -81,6 +82,7 @@ fun main() { fun WindowScope.App(mainStateHolder: MainStateHolder) { val state by mainStateHolder.state.collectAsState() val decoratedWindowScope = this + val textSplitPaneState = rememberSplitPaneState() val screenshotSplitPaneState = rememberSplitPaneState() DisposableEffect(mainStateHolder) { @@ -128,44 +130,19 @@ fun WindowScope.App(mainStateHolder: MainStateHolder) { } MainCategory.Text -> { - val inputTextStateHolder = mainStateHolder.textCommandStateHolder - val inputTextState by inputTextStateHolder.state.collectAsState() - + val inputTextState by mainStateHolder.textCommandStateHolder.state.collectAsState() + val onAction = mainStateHolder.textCommandStateHolder::onAction TextCommandScreen( - // InputText - inputText = inputTextState.userInputText, - onTextChange = { text -> - inputTextStateHolder.updateInputText(text) - }, - isSendingInputText = inputTextState.isSendingUserInputText, - onSendInputText = { - inputTextStateHolder.sendInputText() - }, - canSendInputText = inputTextState.canSendInputText, - canSendTabKey = inputTextState.canSendTabKey, - onSendTabKey = { - inputTextStateHolder.sendTabCommand() - }, - onSaveInputText = { - inputTextStateHolder.saveInputText() - }, - canSaveInputText = inputTextState.canSaveInputText, - // Commands - commands = inputTextState.commands, - onSendCommand = { text -> - inputTextStateHolder.sendTextCommand(text) - }, - canSendCommand = inputTextState.canSendCommand, - isSendingTab = inputTextState.isSendingTab, - onDeleteCommand = { text -> - inputTextStateHolder.deleteInputText(text) - }, + state = inputTextState, + onAction = onAction, + splitterState = textSplitPaneState, ) } MainCategory.Screenshot -> { val screenshotStateHolder = mainStateHolder.screenshotStateHolder val screenshotState by screenshotStateHolder.state.collectAsState() + val onAction = screenshotStateHolder::onAction ScreenshotScreen( screenshot = screenshotState.preview, @@ -174,28 +151,30 @@ fun WindowScope.App(mainStateHolder: MainStateHolder) { canCapture = screenshotState.canExecute, isCapturing = screenshotState.isCapturing, commands = screenshotState.commands, + searchText = screenshotState.searchText, onOpenDirectory = { - screenshotStateHolder.openDirectory() + onAction(ScreenshotAction.OpenDirectory) }, onCopyScreenshot = { - screenshotStateHolder.copyScreenShotToClipboard() + onAction(ScreenshotAction.CopyScreenshotToClipboard) }, onDeleteScreenshot = { - screenshotStateHolder.deleteScreenShotToClipboard() + onAction(ScreenshotAction.DeleteScreenshotToClipboard) }, onTakeScreenshot = { screenshot -> - screenshotStateHolder.takeScreenShot( - screenshot, - ) + onAction(ScreenshotAction.TakeScreenshot(screenshot)) }, onSelectScreenshot = { screenshot -> - screenshotStateHolder.selectScreenshot(screenshot) + onAction(ScreenshotAction.SelectScreenshot(screenshot)) }, onNextScreenshot = { - screenshotStateHolder.nextScreenshot() + onAction(ScreenshotAction.NextScreenshot) }, onPreviousScreenshot = { - screenshotStateHolder.previousScreenshot() + onAction(ScreenshotAction.PreviousScreenshot) + }, + onUpdateSearchText = { + onAction(ScreenshotAction.UpdateSearchText(it)) }, ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/MainStateHolder.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/MainStateHolder.kt index d8a238dd..387546e9 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/MainStateHolder.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/MainStateHolder.kt @@ -60,8 +60,6 @@ class MainStateHolder( private val children: List> = listOf( commandStateHolder, - textCommandStateHolder, - screenshotStateHolder, topStateHolder, ) @@ -74,6 +72,8 @@ class MainStateHolder( override fun setup() { children.forEach { it.setup() } + textCommandStateHolder.onSetup() + screenshotStateHolder.onSetup() } override fun refresh() { @@ -82,10 +82,14 @@ class MainStateHolder( syncLanguage() refreshUseCase() children.forEach { it.refresh() } + textCommandStateHolder.onRefresh() + screenshotStateHolder.onRefresh() } override fun dispose() { children.forEach { it.dispose() } + textCommandStateHolder.onDispose() + screenshotStateHolder.onDispose() } fun saveSetting(windowSize: WindowSize) { diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVI.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVI.kt new file mode 100644 index 00000000..2af314a8 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVI.kt @@ -0,0 +1,24 @@ +package jp.kaleidot725.adbpad.core.mvi + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface MVI { + val coroutineScope: CoroutineScope + val state: StateFlow + val currentState: UiState + val sideEffect: Flow + + fun onSetup() + + fun onAction(uiAction: UiAction) + + fun onRefresh() + + fun onDispose() + + fun update(block: UiState.() -> UiState) + + suspend fun sideEffect(effect: SideEffect) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIAction.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIAction.kt new file mode 100644 index 00000000..09a184c5 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIAction.kt @@ -0,0 +1,3 @@ +package jp.kaleidot725.adbpad.core.mvi + +interface MVIAction diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIDelegate.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIDelegate.kt new file mode 100644 index 00000000..5dbc30c0 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIDelegate.kt @@ -0,0 +1,45 @@ +package jp.kaleidot725.adbpad.core.mvi + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class MVIDelegate internal constructor( + initialUiState: UiState, +) : MVI { + override val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main + Dispatchers.IO) + + private val uiState = MutableStateFlow(initialUiState) + override val state: StateFlow = uiState.asStateFlow() + override val currentState: UiState get() = state.value + private val _sideEffect by lazy { Channel() } + override val sideEffect: Flow by lazy { _sideEffect.receiveAsFlow() } + + override fun onSetup() {} + + override fun onAction(uiAction: UiAction) {} + + override fun onRefresh() {} + + override fun onDispose() {} + + override fun update(block: UiState.() -> UiState) { + uiState.update { block(it) } + } + + override suspend fun sideEffect(effect: SideEffect) { + coroutineScope.launch { _sideEffect.send(effect) } + } +} + +fun mvi( + initialUiState: UiState, +): MVI = MVIDelegate(initialUiState) diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVISideEffect.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVISideEffect.kt new file mode 100644 index 00000000..551ef500 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVISideEffect.kt @@ -0,0 +1,3 @@ +package jp.kaleidot725.adbpad.core.mvi + +interface MVISideEffect diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIState.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIState.kt new file mode 100644 index 00000000..6fa10a65 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/core/mvi/MVIState.kt @@ -0,0 +1,3 @@ +package jp.kaleidot725.adbpad.core.mvi + +interface MVIState diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/di/DomainModule.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/di/DomainModule.kt index 7c7f5ba5..5ef3a059 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/di/DomainModule.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/di/DomainModule.kt @@ -18,11 +18,8 @@ import jp.kaleidot725.adbpad.domain.usecase.screenshot.TakeScreenshotUseCase import jp.kaleidot725.adbpad.domain.usecase.sdkpath.GetSdkPathUseCase import jp.kaleidot725.adbpad.domain.usecase.sdkpath.SaveSdkPathUseCase import jp.kaleidot725.adbpad.domain.usecase.text.AddTextCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.DeleteTextCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.text.ExecuteTextCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.text.GetTextCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.SendTabCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.SendUserInputTextCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.theme.GetDarkModeFlowUseCase import jp.kaleidot725.adbpad.domain.usecase.window.GetWindowSizeUseCase import jp.kaleidot725.adbpad.domain.usecase.window.SaveWindowSizeUseCase @@ -54,9 +51,6 @@ val domainModule = factory { AddTextCommandUseCase(get()) } - factory { - DeleteTextCommandUseCase(get()) - } factory { ExecuteTextCommandUseCase(get()) } @@ -69,9 +63,6 @@ val domainModule = factory { GetScreenshotCommandUseCase(get()) } - factory { - SendUserInputTextCommandUseCase(get()) - } factory { GetWindowSizeUseCase(get()) } @@ -99,9 +90,6 @@ val domainModule = factory { GetLanguageUseCase(get()) } - factory { - SendTabCommandUseCase(get()) - } factory { RefreshUseCase(get(), get(), get()) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/command/TextCommand.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/command/TextCommand.kt index cf3c4ec6..26749ae6 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/command/TextCommand.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/command/TextCommand.kt @@ -1,10 +1,31 @@ package jp.kaleidot725.adbpad.domain.model.command import com.malinskiy.adam.request.shell.v1.ShellCommandRequest +import kotlinx.serialization.Serializable +import java.util.UUID +@Serializable data class TextCommand( + val id: String = UUID.randomUUID().toString(), + val title: String, val text: String, val isRunning: Boolean = false, ) { - val requests: List = listOf(ShellCommandRequest("input text $text")) + val requests: List get() { + return buildList { + val texts = text.split('\n') + texts.forEach { text -> + if (text.isEmpty()) { + add(ShellCommandRequest("")) + } else { + add(ShellCommandRequest("input text $text")) + } + } + } + } + + enum class Option { + SendWithTab, + SendWithNewLine, + } } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/Language.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/Language.kt index 5b594164..c2856f49 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/Language.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/Language.kt @@ -40,6 +40,10 @@ object Language : StringResources { get() = getCurrentResources().light override val system: String get() = getCurrentResources().system + override val search: String + get() = getCurrentResources().search + override val textCommandUnTitle: String + get() = getCurrentResources().textCommandUnTitle override val screenshotTakeByCurrentTheme: String get() = getCurrentResources().screenshotTakeByCurrentTheme override val screenshotTakeByDarkTheme: String @@ -199,19 +203,23 @@ object Language : StringResources { override val adbErrorOpenSetting: String get() = getCurrentResources().adbErrorOpenSetting + override val textCommandOptionNewLine: String + get() = getCurrentResources().textCommandOptionNewLine + override val textCommandOptionTab: String + get() = getCurrentResources().textCommandOptionTab + private var currentType: Type = Type.ENGLISH fun switch(type: Type) { currentType = type } - private fun getCurrentResources(): StringResources { - return when (currentType) { + private fun getCurrentResources(): StringResources = + when (currentType) { Type.ENGLISH -> EnglishResources Type.JAPANESE -> JapaneseResources Type.CHINESE -> ChineseResources } - } enum class Type { ENGLISH, diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/ChineseResources.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/ChineseResources.kt index 24bdfc68..50f31148 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/ChineseResources.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/ChineseResources.kt @@ -20,12 +20,18 @@ object ChineseResources : StringResources { override val dark = "深色" override val light = "浅色" override val system = "系统" + override val search: String = "搜索" + + override val textCommandUnTitle: String = "取消文本标题命令" override val screenshotTakeByCurrentTheme = "按当前主题截图" override val screenshotTakeByDarkTheme = "按深色主题截图" override val screenshotTakeByLightTheme = "按浅色主题截图" override val screenshotTakeByBothTheme = "按两种主题截图" + override val textCommandOptionNewLine: String = "用换行键发送" + override val textCommandOptionTab: String = "用制表符键发送" + override val commandStartEventFormat = "开始发送命令 「%s」" override val commandEndEventFormat = "结束发送命令 「%s」" override val commandErrorEventFormat = "发送命令失败 「%s」" diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/EnglishResources.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/EnglishResources.kt index 3cff56e5..1d66f38d 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/EnglishResources.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/EnglishResources.kt @@ -20,12 +20,18 @@ object EnglishResources : StringResources { override val dark = "Dark" override val light = "Light" override val system = "System" + override val search: String = "Search" + + override val textCommandUnTitle: String = "untitle text command" override val screenshotTakeByCurrentTheme = "Take by current theme" override val screenshotTakeByDarkTheme = "Take by dark theme" override val screenshotTakeByLightTheme = "Take by light theme" override val screenshotTakeByBothTheme = "Take by both theme" + override val textCommandOptionNewLine: String = "Send with newline key" + override val textCommandOptionTab: String = "Send with tab key" + override val commandStartEventFormat = "Start sending command 「%s」" override val commandEndEventFormat = "End sending command 「%s」" override val commandErrorEventFormat = "Error sending command 「%s」" diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/JapaneseResources.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/JapaneseResources.kt index 39fdb00f..f210481b 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/JapaneseResources.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/JapaneseResources.kt @@ -20,12 +20,17 @@ object JapaneseResources : StringResources { override val dark = "Dark" override val light = "Light" override val system = "System" + override val search: String = "Search" + override val textCommandUnTitle: String = "untitle text command" override val screenshotTakeByCurrentTheme = "現在のテーマで撮影する" override val screenshotTakeByDarkTheme = "ダークテーマで撮影する" override val screenshotTakeByLightTheme = "ライトテーマで撮影する" override val screenshotTakeByBothTheme = "両方のテーマで撮影する" + override val textCommandOptionNewLine: String = "送信する(改行キー)" + override val textCommandOptionTab: String = "送信する(タブキー)" + override val commandStartEventFormat = "「%s」のコマンド送信を開始しました" override val commandEndEventFormat = "「%s」のコマンド送信が完了しました" override val commandErrorEventFormat = "「%s」のコマンド送信に失敗しました" diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/StringResources.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/StringResources.kt index 615bc0ea..26672dfc 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/StringResources.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/model/language/resources/StringResources.kt @@ -1,6 +1,6 @@ package jp.kaleidot725.adbpad.domain.model.language.resources -val APP_VERSION = "v1.5.2" +val APP_VERSION = "v2.0.0" interface StringResources { val windowTitle: String @@ -21,7 +21,9 @@ interface StringResources { val dark: String val light: String val system: String + val search: String + val textCommandUnTitle: String val screenshotTakeByCurrentTheme: String val screenshotTakeByDarkTheme: String val screenshotTakeByLightTheme: String @@ -108,4 +110,7 @@ interface StringResources { val adbErrorTitle: String val adbErrorMessage: String val adbErrorOpenSetting: String + + val textCommandOptionNewLine: String + val textCommandOptionTab: String } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/KeyCommandRepository.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/KeyCommandRepository.kt deleted file mode 100644 index 24bd63ad..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/KeyCommandRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package jp.kaleidot725.adbpad.domain.repository - -import jp.kaleidot725.adbpad.domain.model.device.Device - -interface KeyCommandRepository { - suspend fun sendKeyCommand( - device: Device, - keycode: Int, - onStart: suspend () -> Unit, - onComplete: suspend () -> Unit, - onFailed: suspend () -> Unit, - ) -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/TextCommandRepository.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/TextCommandRepository.kt index 2cfe7bb2..044849dd 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/TextCommandRepository.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/repository/TextCommandRepository.kt @@ -8,19 +8,22 @@ interface TextCommandRepository { suspend fun addTextCommand(command: TextCommand): Boolean + suspend fun updateTextCommandTitle( + id: String, + title: String, + ): Boolean + + suspend fun updateTextCommandValue( + id: String, + value: String, + ): Boolean + suspend fun removeTextCommand(command: TextCommand): Boolean suspend fun sendCommand( device: Device, command: TextCommand, - onStart: suspend () -> Unit, - onComplete: suspend () -> Unit, - onFailed: suspend () -> Unit, - ) - - suspend fun sendUserInputText( - device: Device, - text: String, + option: TextCommand.Option, onStart: suspend () -> Unit, onComplete: suspend () -> Unit, onFailed: suspend () -> Unit, diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/SettingFileCreator.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/SettingFileCreator.kt index 98e2b57e..c34a3364 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/SettingFileCreator.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/SettingFileCreator.kt @@ -6,15 +6,13 @@ import jp.kaleidot725.adbpad.domain.model.setting.Appearance import jp.kaleidot725.adbpad.domain.model.setting.SdkPath import jp.kaleidot725.adbpad.domain.model.setting.WindowSize import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File import java.io.IOException object SettingFileCreator { - fun save(setting: Setting): Boolean { - return try { + fun save(setting: Setting): Boolean = + try { createDir() File(getFilePath()).outputStream().apply { this.write(Json.encodeToString(setting).toByteArray()) @@ -24,16 +22,14 @@ object SettingFileCreator { } catch (exception: IOException) { false } - } - fun load(): Setting { - return try { + fun load(): Setting = + try { val content = File(getFilePath()).readText() Json.decodeFromString(string = content) } catch (e: Exception) { Setting() } - } private fun getDirPath() = OSContext.resolveOSContext().directory @@ -53,7 +49,6 @@ object SettingFileCreator { val language: Language.Type = Language.Type.ENGLISH, val appearance: Appearance = Appearance.LIGHT, val sdkPath: SdkPath = SdkPath(), - val inputTexts: List = emptyList(), val windowSize: WindowSize = WindowSize.DEFAULT, ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFactory.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFactory.kt index 2313a839..0e22ee87 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFactory.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFactory.kt @@ -3,7 +3,8 @@ package jp.kaleidot725.adbpad.domain.service import jp.kaleidot725.adbpad.domain.model.command.TextCommand object TextCommandFactory { - fun createNew(text: String): TextCommand { - return TextCommand(text = text, isRunning = false) - } + fun createNew( + title: String, + text: String, + ): TextCommand = TextCommand(title = title, text = text, isRunning = false) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFileCreator.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFileCreator.kt new file mode 100644 index 00000000..c046009b --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/service/TextCommandFileCreator.kt @@ -0,0 +1,48 @@ +package jp.kaleidot725.adbpad.domain.service + +import jp.kaleidot725.adbpad.domain.model.command.TextCommand +import jp.kaleidot725.adbpad.domain.model.os.OSContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.io.File +import java.io.IOException + +object TextCommandFileCreator { + fun save(setting: TextCommandSetting): Boolean = + try { + createDir() + File(getFilePath()).outputStream().apply { + this.write(Json.encodeToString(setting).toByteArray()) + this.close() + } + true + } catch (exception: IOException) { + false + } + + fun load(): TextCommandSetting = + try { + val content = File(getFilePath()).readText() + Json.decodeFromString(string = content) + } catch (e: Exception) { + TextCommandSetting() + } + + private fun getDirPath() = OSContext.resolveOSContext().directory + + private fun getFilePath() = getDirPath() + "text_command.json" + + private fun createDir() { + try { + val file = File(getDirPath()) + if (!file.exists()) file.mkdir() + } catch (e: Exception) { + return + } + } + + @Serializable + data class TextCommandSetting( + val values: List = emptyList(), + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/AddTextCommandUseCase.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/AddTextCommandUseCase.kt index 07190353..4f28576b 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/AddTextCommandUseCase.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/AddTextCommandUseCase.kt @@ -3,8 +3,11 @@ package jp.kaleidot725.adbpad.domain.usecase.text import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository import jp.kaleidot725.adbpad.domain.service.TextCommandFactory -class AddTextCommandUseCase(private val textCommandRepository: TextCommandRepository) { - suspend operator fun invoke(text: String): Boolean { - return textCommandRepository.addTextCommand(TextCommandFactory.createNew(text)) - } +class AddTextCommandUseCase( + private val textCommandRepository: TextCommandRepository, +) { + suspend operator fun invoke( + title: String, + text: String, + ): Boolean = textCommandRepository.addTextCommand(TextCommandFactory.createNew(title, text)) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/DeleteTextCommandUseCase.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/DeleteTextCommandUseCase.kt deleted file mode 100644 index fb36e9c2..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/DeleteTextCommandUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package jp.kaleidot725.adbpad.domain.usecase.text - -import jp.kaleidot725.adbpad.domain.model.command.TextCommand -import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository - -class DeleteTextCommandUseCase(private val textCommandRepository: TextCommandRepository) { - suspend operator fun invoke(command: TextCommand): Boolean { - return textCommandRepository.removeTextCommand(command) - } -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/ExecuteTextCommandUseCase.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/ExecuteTextCommandUseCase.kt index 272fbf06..d32924ec 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/ExecuteTextCommandUseCase.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/ExecuteTextCommandUseCase.kt @@ -10,6 +10,7 @@ class ExecuteTextCommandUseCase( suspend operator fun invoke( device: Device, command: TextCommand, + option: TextCommand.Option, onStart: suspend () -> Unit, onFailed: suspend () -> Unit, onComplete: suspend () -> Unit, @@ -17,6 +18,7 @@ class ExecuteTextCommandUseCase( textCommandRepository.sendCommand( device = device, command = command, + option = option, onStart = { onStart() }, diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendTabCommandUseCase.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendTabCommandUseCase.kt deleted file mode 100644 index 560026fe..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendTabCommandUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package jp.kaleidot725.adbpad.domain.usecase.text - -import jp.kaleidot725.adbpad.domain.model.device.Device -import jp.kaleidot725.adbpad.domain.repository.KeyCommandRepository - -class SendTabCommandUseCase( - private val keyCommandRepository: KeyCommandRepository, -) { - suspend operator fun invoke( - device: Device, - onStart: suspend () -> Unit, - onFailed: suspend () -> Unit, - onComplete: suspend () -> Unit, - ) { - val tabKeyCode = 61 - keyCommandRepository.sendKeyCommand( - device = device, - keycode = tabKeyCode, - onStart = { - onStart() - }, - onFailed = { - onFailed() - }, - onComplete = { - onComplete() - }, - ) - } -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendUserInputTextCommandUseCase.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendUserInputTextCommandUseCase.kt deleted file mode 100644 index a87b3d11..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/domain/usecase/text/SendUserInputTextCommandUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package jp.kaleidot725.adbpad.domain.usecase.text - -import jp.kaleidot725.adbpad.domain.model.device.Device -import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository - -class SendUserInputTextCommandUseCase( - private val textCommandRepository: TextCommandRepository, -) { - suspend operator fun invoke( - device: Device, - text: String, - onStart: suspend () -> Unit, - onFailed: suspend () -> Unit, - onComplete: suspend () -> Unit, - ) { - textCommandRepository.sendUserInputText( - device = device, - text = text, - onStart = { - onStart() - }, - onFailed = { - onFailed() - }, - onComplete = { - onComplete() - }, - ) - } -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/di/RepositoryModule.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/di/RepositoryModule.kt index 37a59964..e0ce15f2 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/di/RepositoryModule.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/di/RepositoryModule.kt @@ -2,14 +2,12 @@ package jp.kaleidot725.adbpad.repository.di import jp.kaleidot725.adbpad.domain.repository.DeviceControlCommandRepository import jp.kaleidot725.adbpad.domain.repository.DeviceRepository -import jp.kaleidot725.adbpad.domain.repository.KeyCommandRepository import jp.kaleidot725.adbpad.domain.repository.NormalCommandRepository import jp.kaleidot725.adbpad.domain.repository.ScreenshotCommandRepository import jp.kaleidot725.adbpad.domain.repository.SettingRepository import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository import jp.kaleidot725.adbpad.repository.impl.DeviceControlCommandRepositoryImpl import jp.kaleidot725.adbpad.repository.impl.DeviceRepositoryImpl -import jp.kaleidot725.adbpad.repository.impl.KeyCommandRepositoryImpl import jp.kaleidot725.adbpad.repository.impl.NormalCommandRepositoryImpl import jp.kaleidot725.adbpad.repository.impl.ScreenshotCommandRepositoryImpl import jp.kaleidot725.adbpad.repository.impl.SettingRepositoryImpl @@ -37,9 +35,6 @@ val repositoryModule = factory { VersionRepository() } - factory { - KeyCommandRepositoryImpl() - } factory { DeviceControlCommandRepositoryImpl() } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/KeyCommandRepositoryImpl.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/KeyCommandRepositoryImpl.kt deleted file mode 100644 index 39d1bfe3..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/KeyCommandRepositoryImpl.kt +++ /dev/null @@ -1,33 +0,0 @@ -package jp.kaleidot725.adbpad.repository.impl - -import com.malinskiy.adam.AndroidDebugBridgeClientFactory -import jp.kaleidot725.adbpad.domain.model.command.KeyCommand -import jp.kaleidot725.adbpad.domain.model.device.Device -import jp.kaleidot725.adbpad.domain.repository.KeyCommandRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class KeyCommandRepositoryImpl : KeyCommandRepository { - private val adbClient = AndroidDebugBridgeClientFactory().build() - - override suspend fun sendKeyCommand( - device: Device, - keycode: Int, - onStart: suspend () -> Unit, - onComplete: suspend () -> Unit, - onFailed: suspend () -> Unit, - ) { - withContext(Dispatchers.IO) { - val command = KeyCommand(keycode) - command.requests.forEach { request -> - val result = adbClient.execute(request, device.serial) - if (result.exitCode != 0) { - onFailed() - return@withContext - } - } - - onComplete() - } - } -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/TextCommandRepositoryImpl.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/TextCommandRepositoryImpl.kt index 94afd0d8..94ebeb99 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/TextCommandRepositoryImpl.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/repository/impl/TextCommandRepositoryImpl.kt @@ -1,10 +1,11 @@ package jp.kaleidot725.adbpad.repository.impl import com.malinskiy.adam.AndroidDebugBridgeClientFactory +import jp.kaleidot725.adbpad.domain.model.command.KeyCommand import jp.kaleidot725.adbpad.domain.model.command.TextCommand import jp.kaleidot725.adbpad.domain.model.device.Device import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository -import jp.kaleidot725.adbpad.domain.service.SettingFileCreator +import jp.kaleidot725.adbpad.domain.service.TextCommandFileCreator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -12,85 +13,124 @@ import kotlinx.coroutines.withContext class TextCommandRepositoryImpl : TextCommandRepository { private val runningCommands: MutableSet = mutableSetOf() private val adbClient = AndroidDebugBridgeClientFactory().build() + private val lock: Any = Any() override suspend fun getAllTextCommand(): List { return withContext(Dispatchers.IO) { - val setting = SettingFileCreator.load() - return@withContext setting.inputTexts.map { text -> - TextCommand(text = text, isRunning = runningCommands.any { it.text == text }) + synchronized(lock) { + val setting = TextCommandFileCreator.load() + return@withContext setting.values.map { text -> + text.copy(isRunning = runningCommands.any { it.id == text.id }) + } } } } override suspend fun addTextCommand(command: TextCommand): Boolean { return withContext(Dispatchers.IO) { - val oldSetting = SettingFileCreator.load() - if (oldSetting.inputTexts.any { it == command.text }) return@withContext true - - val newInputTexts = oldSetting.inputTexts.toMutableList().apply { add(command.text) } - val newSetting = oldSetting.copy(inputTexts = newInputTexts) - return@withContext SettingFileCreator.save(newSetting) + synchronized(lock) { + val oldSetting = TextCommandFileCreator.load() + val newInputTexts = oldSetting.values.toMutableList().apply { add(command) } + val newSetting = oldSetting.copy(values = newInputTexts) + return@withContext TextCommandFileCreator.save(newSetting) + } } } override suspend fun removeTextCommand(command: TextCommand): Boolean { return withContext(Dispatchers.IO) { - val oldSetting = SettingFileCreator.load() - val newInputTexts = oldSetting.inputTexts.toMutableList().apply { remove(command.text) } - val newSetting = oldSetting.copy(inputTexts = newInputTexts) - return@withContext SettingFileCreator.save(newSetting) + synchronized(lock) { + val oldSetting = TextCommandFileCreator.load() + val newInputTexts = oldSetting.values.toMutableList().apply { remove(command) } + val newSetting = oldSetting.copy(values = newInputTexts) + return@withContext TextCommandFileCreator.save(newSetting) + } } } - override suspend fun sendCommand( - device: Device, - command: TextCommand, - onStart: suspend () -> Unit, - onComplete: suspend () -> Unit, - onFailed: suspend () -> Unit, - ) { - withContext(Dispatchers.IO) { - runningCommands.add(command) - onStart() - - delay(300) + override suspend fun updateTextCommandTitle( + id: String, + title: String, + ): Boolean { + return withContext(Dispatchers.IO) { + synchronized(lock) { + val oldSetting = TextCommandFileCreator.load() + val targetIndex = oldSetting.values.indexOfFirst { it.id == id } + val target = oldSetting.values.getOrNull(targetIndex) ?: return@withContext false + val newTarget = target.copy(title = title) + val newCommands = oldSetting.values.toMutableList() + newCommands.remove(target) + newCommands.add(targetIndex, newTarget) - command.requests.forEach { request -> - val result = adbClient.execute(request, device.serial) - if (result.exitCode != 0) { - runningCommands.remove(command) - onFailed() - return@withContext - } + val newSetting = oldSetting.copy(values = newCommands) + return@withContext TextCommandFileCreator.save(newSetting) } + } + } - runningCommands.remove(command) - onComplete() + override suspend fun updateTextCommandValue( + id: String, + text: String, + ): Boolean { + return withContext(Dispatchers.IO) { + synchronized(lock) { + val oldSetting = TextCommandFileCreator.load() + val targetIndex = oldSetting.values.indexOfFirst { it.id == id } + val target = oldSetting.values.getOrNull(targetIndex) ?: return@withContext false + val newTarget = target.copy(text = text) + val newCommands = oldSetting.values.toMutableList() + newCommands.remove(target) + newCommands.add(targetIndex, newTarget) + + val newSetting = oldSetting.copy(values = newCommands) + return@withContext TextCommandFileCreator.save(newSetting) + } } } - override suspend fun sendUserInputText( + override suspend fun sendCommand( device: Device, - text: String, + command: TextCommand, + option: TextCommand.Option, onStart: suspend () -> Unit, onComplete: suspend () -> Unit, onFailed: suspend () -> Unit, ) { withContext(Dispatchers.IO) { + runningCommands.add(command) onStart() delay(300) - val command = TextCommand(text) - command.requests.forEach { request -> - val result = adbClient.execute(request, device.serial) - if (result.exitCode != 0) { - runningCommands.remove(command) - onFailed() - return@withContext + command.requests.forEachIndexed { index, request -> + if (request.cmd.isNotEmpty()) { + val result = adbClient.execute(request, device.serial) + if (result.exitCode != 0) { + runningCommands.remove(command) + onFailed() + return@withContext + } + } + + if (command.requests.lastIndex != index && TextCommand.Option.SendWithNewLine != option) { + val keyCode = + when (option) { + TextCommand.Option.SendWithTab -> 61 + TextCommand.Option.SendWithNewLine -> 66 + } + + val keyCommand = KeyCommand(keyCode) + keyCommand.requests.forEach { keyRequest -> + val keyResult = adbClient.execute(keyRequest, device.serial) + if (keyResult.exitCode != 0) { + onFailed() + return@withContext + } + } } } + runningCommands.remove(command) onComplete() } } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/common/dummy/TextCommandDummy.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/common/dummy/TextCommandDummy.kt new file mode 100644 index 00000000..c2e164e8 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/common/dummy/TextCommandDummy.kt @@ -0,0 +1,27 @@ +package jp.kaleidot725.adbpad.ui.common.dummy + +import jp.kaleidot725.adbpad.domain.model.command.TextCommand + +object TextCommandDummy { + val value = + TextCommand( + title = "TITLE", + text = "TEXT", + ) + + val values = + listOf( + TextCommand( + title = "TITLE", + text = "TEXT", + ), + TextCommand( + title = "TITLE", + text = "TEXT", + ), + TextCommand( + title = "TITLE", + text = "TEXT", + ), + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/component/DefaultTextField.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/component/DefaultTextField.kt new file mode 100644 index 00000000..e7e562a8 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/component/DefaultTextField.kt @@ -0,0 +1,62 @@ +package jp.kaleidot725.adbpad.ui.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp + +@Composable +fun DefaultTextField( + id: String = "", + initialText: String, + placeHolder: String, + onUpdateText: (String) -> Unit, + maxLines: Int = 1, + modifier: Modifier = Modifier, +) { + var localText by remember(id) { mutableStateOf(initialText) } + + Box(modifier) { + if (localText.isEmpty()) { + Text( + text = placeHolder, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f), + fontSize = 16.sp, + modifier = Modifier.align(Alignment.CenterStart), + ) + } + + BasicTextField( + value = localText, + onValueChange = { + localText = it + onUpdateText(it) + }, + maxLines = maxLines, + textStyle = TextStyle(color = MaterialTheme.colors.onSurface, fontSize = 16.sp), + cursorBrush = SolidColor(MaterialTheme.colors.onSurface), + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Preview +@Composable +private fun Preview() { + DefaultTextField( + initialText = "", + placeHolder = "Search", + onUpdateText = {}, + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/di/StateHolderModule.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/di/StateHolderModule.kt index 43dc34aa..6898f175 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/di/StateHolderModule.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/di/StateHolderModule.kt @@ -20,13 +20,10 @@ val stateHolderModule = factory { TextCommandStateHolder( - addTextCommandUseCase = get(), - deleteTextCommandUseCase = get(), + textCommandRepository = get(), getTextCommandUseCase = get(), executeTextCommandUseCase = get(), getSelectedDeviceFlowUseCase = get(), - sendUserInputTextCommandUseCase = get(), - sendTabCommandUseCase = get(), ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotAction.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotAction.kt new file mode 100644 index 00000000..83fa8ca4 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotAction.kt @@ -0,0 +1,29 @@ +package jp.kaleidot725.adbpad.ui.screen.screenshot + +import jp.kaleidot725.adbpad.core.mvi.MVIAction +import jp.kaleidot725.adbpad.domain.model.command.ScreenshotCommand +import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot + +sealed class ScreenshotAction : MVIAction { + data class UpdateSearchText( + val text: String, + ) : ScreenshotAction() + + data class TakeScreenshot( + val command: ScreenshotCommand, + ) : ScreenshotAction() + + data object OpenDirectory : ScreenshotAction() + + data object CopyScreenshotToClipboard : ScreenshotAction() + + data object DeleteScreenshotToClipboard : ScreenshotAction() + + data class SelectScreenshot( + val screenshot: Screenshot, + ) : ScreenshotAction() + + data object NextScreenshot : ScreenshotAction() + + data object PreviousScreenshot : ScreenshotAction() +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotScreen.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotScreen.kt index a6d4058a..3be0aedb 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotScreen.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotScreen.kt @@ -9,8 +9,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Divider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,7 +22,9 @@ import androidx.compose.ui.unit.dp import jp.kaleidot725.adbpad.domain.model.UserColor import jp.kaleidot725.adbpad.domain.model.command.ScreenshotCommand import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot +import jp.kaleidot725.adbpad.ui.common.resource.defaultBorder import jp.kaleidot725.adbpad.ui.screen.screenshot.component.ScreenshotExplorer +import jp.kaleidot725.adbpad.ui.screen.screenshot.component.ScreenshotHeader import jp.kaleidot725.adbpad.ui.screen.screenshot.component.ScreenshotMenu import jp.kaleidot725.adbpad.ui.screen.screenshot.component.ScreenshotViewer import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi @@ -29,7 +33,7 @@ import org.jetbrains.compose.splitpane.SplitPaneState import org.jetbrains.compose.splitpane.rememberSplitPaneState import java.awt.Cursor -private fun Modifier.cursorForHorizontalResize(): Modifier = pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) +fun Modifier.cursorForHorizontalResize(): Modifier = pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) @OptIn(ExperimentalSplitPaneApi::class) @Composable @@ -40,6 +44,7 @@ fun ScreenshotScreen( canCapture: Boolean, isCapturing: Boolean, commands: List, + searchText: String, onOpenDirectory: () -> Unit, onCopyScreenshot: () -> Unit, onDeleteScreenshot: () -> Unit, @@ -47,6 +52,7 @@ fun ScreenshotScreen( onSelectScreenshot: (Screenshot) -> Unit, onNextScreenshot: () -> Unit, onPreviousScreenshot: () -> Unit, + onUpdateSearchText: (String) -> Unit, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -60,14 +66,24 @@ fun ScreenshotScreen( .weight(1.0f), ) { first(minSize = 350.dp) { - ScreenshotExplorer( - selectedScreenshot = screenshot, - screenshots = screenshots, - onSelectScreenShot = onSelectScreenshot, - onNextScreenshot = onNextScreenshot, - onPreviousScreenshot = onPreviousScreenshot, - modifier = Modifier.fillMaxSize(), - ) + Column { + ScreenshotHeader( + searchText = searchText, + onUpdateSearchText = onUpdateSearchText, + modifier = Modifier, + ) + + Divider(modifier = Modifier.height(1.dp).fillMaxWidth().defaultBorder()) + + ScreenshotExplorer( + selectedScreenshot = screenshot, + screenshots = screenshots, + onSelectScreenShot = onSelectScreenshot, + onNextScreenshot = onNextScreenshot, + onPreviousScreenshot = onPreviousScreenshot, + modifier = Modifier.fillMaxSize(), + ) + } } second { @@ -130,6 +146,7 @@ private fun ScreenshotScreen_Preview() { canCapture = true, isCapturing = false, commands = emptyList(), + searchText = "", onOpenDirectory = {}, onCopyScreenshot = {}, onDeleteScreenshot = {}, @@ -137,5 +154,6 @@ private fun ScreenshotScreen_Preview() { onSelectScreenshot = {}, onNextScreenshot = {}, onPreviousScreenshot = {}, + onUpdateSearchText = {}, ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotSideEffect.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotSideEffect.kt new file mode 100644 index 00000000..e1ed5575 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotSideEffect.kt @@ -0,0 +1,5 @@ +package jp.kaleidot725.adbpad.ui.screen.screenshot + +import jp.kaleidot725.adbpad.core.mvi.MVISideEffect + +sealed class ScreenshotSideEffect : MVISideEffect diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotState.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotState.kt index cb6f074b..d3a39dc6 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotState.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotState.kt @@ -1,15 +1,17 @@ package jp.kaleidot725.adbpad.ui.screen.screenshot +import jp.kaleidot725.adbpad.core.mvi.MVIState import jp.kaleidot725.adbpad.domain.model.command.ScreenshotCommand import jp.kaleidot725.adbpad.domain.model.device.Device import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot data class ScreenshotState( + val searchText: String = "", val preview: Screenshot = Screenshot(null), val previews: List = emptyList(), val commands: List = emptyList(), val selectedDevice: Device? = null, val isCapturing: Boolean = false, -) { +) : MVIState { val canExecute: Boolean = selectedDevice != null } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotStateHolder.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotStateHolder.kt index a8203bb9..4b56fd73 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotStateHolder.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/ScreenshotStateHolder.kt @@ -1,7 +1,8 @@ package jp.kaleidot725.adbpad.ui.screen.screenshot +import jp.kaleidot725.adbpad.core.mvi.MVI +import jp.kaleidot725.adbpad.core.mvi.mvi import jp.kaleidot725.adbpad.domain.model.command.ScreenshotCommand -import jp.kaleidot725.adbpad.domain.model.device.Device import jp.kaleidot725.adbpad.domain.model.os.OSContext import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot import jp.kaleidot725.adbpad.domain.repository.ScreenshotCommandRepository @@ -9,17 +10,11 @@ import jp.kaleidot725.adbpad.domain.usecase.device.GetSelectedDeviceFlowUseCase import jp.kaleidot725.adbpad.domain.usecase.screenshot.GetScreenshotCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.screenshot.TakeScreenshotUseCase import jp.kaleidot725.adbpad.domain.utils.ClipBoardUtils -import jp.kaleidot725.adbpad.ui.common.ChildStateHolder -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.awt.Desktop import java.io.File @@ -28,110 +23,145 @@ class ScreenshotStateHolder( private val getScreenshotCommandUseCase: GetScreenshotCommandUseCase, private val getSelectedDeviceFlowUseCase: GetSelectedDeviceFlowUseCase, private val screenshotCommandRepository: ScreenshotCommandRepository, -) : ChildStateHolder { - private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main + Dispatchers.IO) - private val commands: MutableStateFlow> = MutableStateFlow(emptyList()) - private val preview: MutableStateFlow = MutableStateFlow(Screenshot(null)) - private val previews: MutableStateFlow> = MutableStateFlow(emptyList()) - private val isCapturing: MutableStateFlow = MutableStateFlow(false) - private val selectedDevice: StateFlow = - getSelectedDeviceFlowUseCase() - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) - - override val state: StateFlow = - combine( - preview, - previews, - commands, - selectedDevice, - isCapturing, - ) { preview, previews, commands, selectedDevice, isCapturing -> - ScreenshotState(preview, previews, commands, selectedDevice, isCapturing) - }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), ScreenshotState()) - - override fun setup() { +) : MVI by mvi(initialUiState = ScreenshotState()) { + override fun onSetup() { coroutineScope.launch { - commands.value = getScreenshotCommandUseCase() + val commands = getScreenshotCommandUseCase() + update { copy(commands = commands) } + initPreviews() } + + coroutineScope.launch { + getSelectedDeviceFlowUseCase().collectLatest { + update { copy(selectedDevice = it) } + } + } } - override fun refresh() { + override fun onRefresh() { coroutineScope.launch { - commands.value = getScreenshotCommandUseCase() + val commands = getScreenshotCommandUseCase() + update { copy(commands = commands) } + initPreviews() } } - override fun dispose() { + override fun onDispose() { coroutineScope.cancel() } - fun takeScreenShot(command: ScreenshotCommand) { - val selectedDevice = state.value.selectedDevice ?: return + override fun onAction(uiAction: ScreenshotAction) { coroutineScope.launch { - takeScreenshotUseCase( - device = selectedDevice, - command = command, - onStart = { - commands.value = getScreenshotCommandUseCase() - preview.value = Screenshot.EMPTY - isCapturing.value = true - }, - onFailed = { - commands.value = getScreenshotCommandUseCase() - preview.value = Screenshot.EMPTY - isCapturing.value = false - }, - onComplete = { - commands.value = getScreenshotCommandUseCase() - preview.value = it - previews.value = screenshotCommandRepository.getScreenshots() - isCapturing.value = false - }, - ) + when (uiAction) { + is ScreenshotAction.TakeScreenshot -> takeScreenShot(uiAction.command) + ScreenshotAction.OpenDirectory -> openDirectory() + ScreenshotAction.CopyScreenshotToClipboard -> copyScreenShotToClipboard() + ScreenshotAction.DeleteScreenshotToClipboard -> deleteScreenShotToClipboard() + is ScreenshotAction.SelectScreenshot -> selectScreenshot(uiAction.screenshot) + ScreenshotAction.NextScreenshot -> nextScreenshot() + ScreenshotAction.PreviousScreenshot -> previousScreenshot() + is ScreenshotAction.UpdateSearchText -> updateSearchText(uiAction.text) + } } } - fun openDirectory() { - coroutineScope.launch { - val file = File(OSContext.resolveOSContext().screenshotDirectory) - Desktop.getDesktop().open(file) + private suspend fun updateSearchText(searchText: String) { + val screenshots = screenshotCommandRepository.getScreenshots() + update { + copy( + searchText = searchText, + previews = screenshots.filter { it.file?.name?.startsWith(searchText) ?: false }, + ) } } - fun copyScreenShotToClipboard() { - coroutineScope.launch { - val file = preview.value.file ?: return@launch - ClipBoardUtils.copyFile(file) - } + private suspend fun takeScreenShot(command: ScreenshotCommand) { + val selectedDevice = state.value.selectedDevice ?: return + takeScreenshotUseCase( + device = selectedDevice, + command = command, + onStart = { + val commands = getScreenshotCommandUseCase() + update { + copy( + commands = commands, + preview = Screenshot.EMPTY, + isCapturing = true, + ) + } + }, + onFailed = { + val commands = getScreenshotCommandUseCase() + update { + copy( + commands = commands, + preview = Screenshot.EMPTY, + isCapturing = false, + ) + } + }, + onComplete = { + val commands = getScreenshotCommandUseCase() + val screenshots = screenshotCommandRepository.getScreenshots() + update { + copy( + commands = commands, + preview = it, + previews = screenshots, + isCapturing = false, + ) + } + }, + ) } - fun deleteScreenShotToClipboard() { - coroutineScope.launch { - screenshotCommandRepository.delete(preview.value) - initPreviews() - } + private suspend fun openDirectory() { + val file = File(OSContext.resolveOSContext().screenshotDirectory) + withContext(Dispatchers.IO) { Desktop.getDesktop().open(file) } + } + + private fun copyScreenShotToClipboard() { + val file = currentState.preview.file ?: return + ClipBoardUtils.copyFile(file) + } + + private suspend fun deleteScreenShotToClipboard() { + screenshotCommandRepository.delete(currentState.preview) + initPreviews() } - fun selectScreenshot(screenshot: Screenshot) { - preview.value = screenshot + private fun selectScreenshot(screenshot: Screenshot) { + update { + this.copy(preview = screenshot) + } } - fun nextScreenshot() { - val nextIndex = previews.value.indexOf(preview.value) + 1 - val nextPreview = previews.value.getOrNull(nextIndex) ?: return - preview.value = nextPreview + private fun nextScreenshot() { + val nextIndex = currentState.previews.indexOf(currentState.preview) + 1 + val nextPreview = currentState.previews.getOrNull(nextIndex) ?: return + update { + this.copy(preview = nextPreview) + } } - fun previousScreenshot() { - val previousIndex = previews.value.indexOf(preview.value) - 1 - val previousPreview = previews.value.getOrNull(previousIndex) ?: return - preview.value = previousPreview + private fun previousScreenshot() { + val previousIndex = currentState.previews.indexOf(currentState.preview) - 1 + val previousPreview = currentState.previews.getOrNull(previousIndex) ?: return + update { + this.copy(preview = previousPreview) + } } private suspend fun initPreviews() { - previews.value = screenshotCommandRepository.getScreenshots() - preview.value = previews.value.firstOrNull() ?: Screenshot(null) + val screenshots = screenshotCommandRepository.getScreenshots() + val screenshot = screenshots.first() + update { + this.copy( + previews = screenshots, + preview = screenshot, + ) + } } } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotActions.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotActions.kt new file mode 100644 index 00000000..7ec6737f --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotActions.kt @@ -0,0 +1,105 @@ +package jp.kaleidot725.adbpad.ui.screen.screenshot.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FileCopy +import androidx.compose.material.icons.filled.FileOpen +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun ScreenshotActions( + enabled: Boolean, + onOpen: () -> Unit, + onCopy: () -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + IconButton( + onClick = onOpen, + enabled = enabled, + modifier = + Modifier + .padding(vertical = 4.dp) + .size(32.dp) + .clip(RoundedCornerShape(8.dp)) + .align(Alignment.CenterVertically), + ) { + Icon( + imageVector = Icons.Default.FileOpen, + contentDescription = "copy", + modifier = Modifier.height(20.dp), + ) + } + + IconButton( + onClick = onCopy, + enabled = enabled, + modifier = + Modifier + .padding(vertical = 4.dp) + .size(32.dp) + .clip(RoundedCornerShape(8.dp)) + .align(Alignment.CenterVertically), + ) { + Icon( + imageVector = Icons.Default.FileCopy, + contentDescription = "copy", + modifier = Modifier.height(20.dp), + ) + } + + IconButton( + onClick = onDelete, + enabled = enabled, + modifier = + Modifier + .padding(vertical = 4.dp) + .size(32.dp) + .clip(RoundedCornerShape(8.dp)) + .align(Alignment.CenterVertically), + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "delete", + modifier = Modifier.height(20.dp), + ) + } + } +} + +@Preview +@Composable +private fun ScreenshotHeader_Preview() { + ScreenshotActions( + enabled = true, + onOpen = {}, + onCopy = {}, + onDelete = {}, + modifier = + Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(Color.Gray), + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotExplorer.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotExplorer.kt index f72b437e..2665e9de 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotExplorer.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotExplorer.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -25,7 +24,6 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot import jp.kaleidot725.adbpad.ui.common.resource.clickableBackground @@ -44,19 +42,23 @@ fun ScreenshotExplorer( LazyColumn( state = lazyColumnState, modifier = - Modifier.onKeyEvent { event -> - when { - event.key == Key.DirectionUp && event.type == KeyEventType.KeyDown -> { - onPreviousScreenshot() - true - } - event.key == Key.DirectionDown && event.type == KeyEventType.KeyDown -> { - onNextScreenshot() - true + Modifier + .padding(4.dp) + .onKeyEvent { event -> + when { + event.key == Key.DirectionUp && event.type == KeyEventType.KeyDown -> { + onPreviousScreenshot() + true + } + + event.key == Key.DirectionDown && event.type == KeyEventType.KeyDown -> { + onNextScreenshot() + true + } + + else -> false } - else -> false - } - }, + }, ) { items( items = screenshots, @@ -69,17 +71,10 @@ fun ScreenshotExplorer( .clickableBackground( isSelected = selectedScreenshot == screenshot, shape = RoundedCornerShape(4.dp), - ) - .clickable { onSelectScreenShot(screenshot) } + ).clickable { onSelectScreenShot(screenshot) } .padding(horizontal = 12.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - AsyncImage( - model = screenshot.file, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - Text( text = screenshot.file?.name ?: "", ) diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotHeader.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotHeader.kt index 1a4cb1b6..15125f43 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotHeader.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotHeader.kt @@ -1,105 +1,45 @@ package jp.kaleidot725.adbpad.ui.screen.screenshot.component import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FileCopy -import androidx.compose.material.icons.filled.FileOpen +import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.component.DefaultTextField @Composable fun ScreenshotHeader( - enabled: Boolean, - onOpen: () -> Unit, - onCopy: () -> Unit, - onDelete: () -> Unit, + searchText: String, + onUpdateSearchText: (String) -> Unit, modifier: Modifier = Modifier, ) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier, - ) { - IconButton( - onClick = onOpen, - enabled = enabled, - modifier = - Modifier - .padding(vertical = 4.dp) - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .align(Alignment.CenterVertically), - ) { - Icon( - imageVector = Icons.Default.FileOpen, - contentDescription = "copy", - modifier = Modifier.height(20.dp), - ) - } + Row(modifier) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically).padding(12.dp), + ) - IconButton( - onClick = onCopy, - enabled = enabled, - modifier = - Modifier - .padding(vertical = 4.dp) - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .align(Alignment.CenterVertically), - ) { - Icon( - imageVector = Icons.Default.FileCopy, - contentDescription = "copy", - modifier = Modifier.height(20.dp), - ) - } - - IconButton( - onClick = onDelete, - enabled = enabled, - modifier = - Modifier - .padding(vertical = 4.dp) - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .align(Alignment.CenterVertically), - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "delete", - modifier = Modifier.height(20.dp), - ) - } + DefaultTextField( + initialText = searchText, + onUpdateText = onUpdateSearchText, + placeHolder = Language.search, + modifier = Modifier.align(Alignment.CenterVertically).weight(1.0f), + ) } } @Preview @Composable -private fun ScreenshotHeader_Preview() { +private fun Preview() { ScreenshotHeader( - enabled = true, - onOpen = {}, - onCopy = {}, - onDelete = {}, - modifier = - Modifier - .fillMaxWidth() - .wrapContentHeight() - .background(Color.Gray), + searchText = "TEST", + onUpdateSearchText = {}, ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotViewer.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotViewer.kt index 7ac0bd4d..fe1ce3c0 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotViewer.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/screenshot/component/ScreenshotViewer.kt @@ -2,15 +2,22 @@ package jp.kaleidot725.adbpad.ui.screen.screenshot.component import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import jp.kaleidot725.adbpad.domain.model.language.Language import jp.kaleidot725.adbpad.domain.model.screenshot.Screenshot +import jp.kaleidot725.adbpad.ui.common.resource.defaultBorder @Composable fun ScreenshotViewer( @@ -21,15 +28,17 @@ fun ScreenshotViewer( onDeleteScreenshot: () -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier) { - ScreenshotHeader( + Column(modifier) { + ScreenshotActions( enabled = screenshot.file != null, onOpen = onOpenDirectory, onCopy = onCopyScreenshot, onDelete = onDeleteScreenshot, - modifier = Modifier.align(Alignment.TopEnd), + modifier = Modifier.height(48.dp).padding(horizontal = 12.dp).align(Alignment.End), ) + Divider(modifier = Modifier.height(1.dp).fillMaxWidth().defaultBorder()) + if (isCapturing) { Box(modifier = Modifier.fillMaxSize()) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandAction.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandAction.kt new file mode 100644 index 00000000..e6ca2815 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandAction.kt @@ -0,0 +1,38 @@ +package jp.kaleidot725.adbpad.ui.screen.text + +import jp.kaleidot725.adbpad.core.mvi.MVIAction +import jp.kaleidot725.adbpad.domain.model.command.TextCommand + +sealed class TextCommandAction : MVIAction { + data class UpdateSearchText( + val text: String, + ) : TextCommandAction() + + data object AddNewText : TextCommandAction() + + data class UpdateCommandTitle( + val id: String, + val value: String, + ) : TextCommandAction() + + data class UpdateCommandText( + val id: String, + val value: String, + ) : TextCommandAction() + + data object SendTextCommand : TextCommandAction() + + data object DeleteSelectedCommandText : TextCommandAction() + + data object NextCommand : TextCommandAction() + + data class SelectCommand( + val command: TextCommand, + ) : TextCommandAction() + + data object PreviousCommand : TextCommandAction() + + data class UpdateTextCommandOption( + val value: TextCommand.Option, + ) : TextCommandAction() +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandScreen.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandScreen.kt index f99c234e..0271a217 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandScreen.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandScreen.kt @@ -1,80 +1,123 @@ package jp.kaleidot725.adbpad.ui.screen.text import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import jp.kaleidot725.adbpad.domain.model.command.TextCommand -import jp.kaleidot725.adbpad.ui.screen.text.component.InputTextActionMenu +import jp.kaleidot725.adbpad.domain.model.UserColor +import jp.kaleidot725.adbpad.ui.common.resource.defaultBorder +import jp.kaleidot725.adbpad.ui.screen.screenshot.cursorForHorizontalResize +import jp.kaleidot725.adbpad.ui.screen.text.component.TextCommandActions +import jp.kaleidot725.adbpad.ui.screen.text.component.TextCommandEditor +import jp.kaleidot725.adbpad.ui.screen.text.component.TextCommandHeader import jp.kaleidot725.adbpad.ui.screen.text.component.TextCommandList +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.compose.splitpane.SplitPaneState +import org.jetbrains.compose.splitpane.rememberSplitPaneState +@OptIn(ExperimentalSplitPaneApi::class) @Composable fun TextCommandScreen( - // InputText - inputText: String, - onTextChange: (String) -> Unit, - isSendingInputText: Boolean, - onSendInputText: () -> Unit, - canSendInputText: Boolean, - onSaveInputText: () -> Unit, - canSaveInputText: Boolean, - canSendTabKey: Boolean, - onSendTabKey: () -> Unit, - isSendingTab: Boolean, - // Commands - commands: List, - onSendCommand: (TextCommand) -> Unit, - canSendCommand: Boolean, - onDeleteCommand: (TextCommand) -> Unit, + state: TextCommandState, + onAction: (TextCommandAction) -> Unit, + splitterState: SplitPaneState, ) { - Box(modifier = Modifier.fillMaxSize().padding(16.dp)) { - TextCommandList( - commands = commands, - onSend = onSendCommand, - canSend = canSendCommand, - onDelete = onDeleteCommand, - modifier = Modifier.fillMaxSize().padding(bottom = 60.dp), - ) + HorizontalSplitPane( + splitPaneState = splitterState, + modifier = Modifier.fillMaxSize(), + ) { + first(minSize = 350.dp) { + Column { + TextCommandHeader( + searchText = state.searchText, + onUpdateSearchText = { onAction(TextCommandAction.UpdateSearchText(it)) }, + onAddNewTextCommand = { onAction(TextCommandAction.AddNewText) }, + ) - InputTextActionMenu( - inputText = inputText, - onTextChange = onTextChange, - isSending = isSendingInputText, - onSend = onSendInputText, - canSend = canSendInputText, - onSendTab = onSendTabKey, - isSendingTag = isSendingTab, - canSendTab = canSendTabKey, - onSave = onSaveInputText, - canSave = canSaveInputText, - modifier = Modifier.height(50.dp).fillMaxWidth().align(Alignment.BottomEnd), - ) + Divider(modifier = Modifier.fillMaxWidth().defaultBorder()) + + TextCommandList( + selectedCommand = state.selectedCommand, + commands = state.commands, + onSelectCommand = { onAction(TextCommandAction.SelectCommand(it)) }, + onNextCommand = { onAction(TextCommandAction.NextCommand) }, + onPreviousCommand = { onAction(TextCommandAction.PreviousCommand) }, + modifier = Modifier.fillMaxSize().padding(top = 2.dp), + ) + } + } + + second { + Column(modifier = Modifier.background(MaterialTheme.colors.surface)) { + if (state.selectedCommand != null) { + TextCommandEditor( + command = state.selectedCommand, + onUpdateTitle = { id, title -> onAction(TextCommandAction.UpdateCommandTitle(id, title)) }, + onUpdateText = { id, text -> onAction(TextCommandAction.UpdateCommandText(id, text)) }, + onDelete = { onAction(TextCommandAction.DeleteSelectedCommandText) }, + ) + + Spacer( + modifier = Modifier.weight(1.0f), + ) + + TextCommandActions( + command = state.selectedCommand, + canSend = state.canSend, + onSendText = { onAction(TextCommandAction.SendTextCommand) }, + selectedOption = state.selectedTextCommandOption, + onUpdateTextCommandOption = { onAction(TextCommandAction.UpdateTextCommandOption(it)) }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + + splitter { + visiblePart { + Box( + Modifier + .width(1.dp) + .fillMaxHeight() + .border(BorderStroke(1.dp, UserColor.getSplitterColor())), + ) + } + + handle { + Box( + Modifier + .markAsHandle() + .cursorForHorizontalResize() + .width(10.dp) + .fillMaxHeight() + .markAsHandle(), + ) + } + } } } +@OptIn(ExperimentalSplitPaneApi::class) @Preview @Composable private fun InputTextScreen_Preview() { TextCommandScreen( - inputText = "SAMPLE INPUT TEXT", - onTextChange = {}, - isSendingInputText = false, - onSendInputText = {}, - onSaveInputText = {}, - canSaveInputText = true, - commands = listOf(TextCommand("TEST1"), TextCommand("TEST2")), - onSendCommand = {}, - isSendingTab = false, - canSendCommand = true, - canSendInputText = true, - onDeleteCommand = {}, - canSendTabKey = false, - onSendTabKey = {}, + state = TextCommandState(), + onAction = {}, + splitterState = rememberSplitPaneState(), ) } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandSideEffect.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandSideEffect.kt new file mode 100644 index 00000000..1cf1818d --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandSideEffect.kt @@ -0,0 +1,5 @@ +package jp.kaleidot725.adbpad.ui.screen.text + +import jp.kaleidot725.adbpad.core.mvi.MVISideEffect + +sealed class TextCommandSideEffect : MVISideEffect diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandState.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandState.kt index e0f15025..c28547de 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandState.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandState.kt @@ -1,17 +1,19 @@ package jp.kaleidot725.adbpad.ui.screen.text +import jp.kaleidot725.adbpad.core.mvi.MVIState import jp.kaleidot725.adbpad.domain.model.command.TextCommand import jp.kaleidot725.adbpad.domain.model.device.Device data class TextCommandState( + val selectedCommandIndex: Int? = null, val commands: List = emptyList(), val userInputText: String = "", val isSendingUserInputText: Boolean = false, val selectedDevice: Device? = null, val isSendingTab: Boolean = false, -) { - val canSendCommand: Boolean get() = selectedDevice != null - val canSendInputText: Boolean get() = selectedDevice != null && userInputText.isNotEmpty() - val canSaveInputText: Boolean get() = userInputText.isNotEmpty() - val canSendTabKey: Boolean get() = selectedDevice != null + val searchText: String = "", + val selectedTextCommandOption: TextCommand.Option = TextCommand.Option.SendWithTab, +) : MVIState { + val selectedCommand: TextCommand? = commands.getOrNull(selectedCommandIndex ?: 0) + val canSend: Boolean = selectedDevice != null } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandStateHolder.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandStateHolder.kt index 07751535..91f4d5b4 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandStateHolder.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/TextCommandStateHolder.kt @@ -1,127 +1,202 @@ package jp.kaleidot725.adbpad.ui.screen.text +import jp.kaleidot725.adbpad.core.mvi.MVI +import jp.kaleidot725.adbpad.core.mvi.mvi import jp.kaleidot725.adbpad.domain.model.command.TextCommand -import jp.kaleidot725.adbpad.domain.model.device.Device +import jp.kaleidot725.adbpad.domain.repository.TextCommandRepository import jp.kaleidot725.adbpad.domain.usecase.device.GetSelectedDeviceFlowUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.AddTextCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.DeleteTextCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.text.ExecuteTextCommandUseCase import jp.kaleidot725.adbpad.domain.usecase.text.GetTextCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.SendTabCommandUseCase -import jp.kaleidot725.adbpad.domain.usecase.text.SendUserInputTextCommandUseCase -import jp.kaleidot725.adbpad.ui.common.ChildStateHolder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class TextCommandStateHolder( - private val addTextCommandUseCase: AddTextCommandUseCase, - private val deleteTextCommandUseCase: DeleteTextCommandUseCase, + private val textCommandRepository: TextCommandRepository, private val getTextCommandUseCase: GetTextCommandUseCase, private val executeTextCommandUseCase: ExecuteTextCommandUseCase, - private val sendUserInputTextCommandUseCase: SendUserInputTextCommandUseCase, - private val sendTabCommandUseCase: SendTabCommandUseCase, private val getSelectedDeviceFlowUseCase: GetSelectedDeviceFlowUseCase, -) : ChildStateHolder { - private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main + Dispatchers.IO) - private val commands: MutableStateFlow> = MutableStateFlow(emptyList()) - private val userInputText: MutableStateFlow = MutableStateFlow("") - private val isSending: MutableStateFlow = MutableStateFlow(false) - private val isSendingTag: MutableStateFlow = MutableStateFlow(false) - private val selectedDevice: StateFlow = - getSelectedDeviceFlowUseCase() - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) - - override val state: StateFlow = - combine( - commands, - userInputText, - isSending, - isSendingTag, - selectedDevice, - ) { inputTexts, userInputText, isSending, isSendingTag, selectedDevice -> - TextCommandState(inputTexts, userInputText, isSending, selectedDevice, isSendingTag) - }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), TextCommandState()) - - override fun setup() { +) : MVI by mvi(initialUiState = TextCommandState()) { + override fun onSetup() { coroutineScope.launch { - commands.value = getTextCommandUseCase() + getSelectedDeviceFlowUseCase().collect { + update { this.copy(selectedDevice = it) } + } + } + coroutineScope.launch { + val commands = getTextCommandUseCase() + update { this.copy(commands = commands) } } } - override fun refresh() { + override fun onRefresh() { coroutineScope.launch { - commands.value = getTextCommandUseCase() + val commands = getTextCommandUseCase() + update { + this.copy(commands = commands) + } } } - override fun dispose() { + override fun onDispose() { coroutineScope.cancel() } - private val ascii = (0..255).map { it.toChar() } + override fun onAction(uiAction: TextCommandAction) { + coroutineScope.launch { + when (uiAction) { + is TextCommandAction.DeleteSelectedCommandText -> { + deleteInputText() + } + + is TextCommandAction.SendTextCommand -> { + sendTextCommand() + } + + is TextCommandAction.NextCommand -> { + nextCommand() + } + + is TextCommandAction.PreviousCommand -> { + previousCommand() + } + + is TextCommandAction.SelectCommand -> { + selectCommand(uiAction.command) + } + + TextCommandAction.AddNewText -> { + addNewTextCommand() + } - fun updateInputText(text: String) { - val isAscii = text.none { it !in ascii } - if (isAscii) this.userInputText.value = text + is TextCommandAction.UpdateSearchText -> { + updateSearchText(uiAction.text) + } + + is TextCommandAction.UpdateCommandText -> { + updateTextCommandValue(uiAction.id, uiAction.value) + } + + is TextCommandAction.UpdateCommandTitle -> { + updateTextCommandTitle(uiAction.id, uiAction.value) + } + + is TextCommandAction.UpdateTextCommandOption -> { + updateTextCommandOption(uiAction.value) + } + } + } } - fun sendTextCommand(command: TextCommand) { - val selectedDevice = state.value.selectedDevice ?: return - coroutineScope.launch { - executeTextCommandUseCase( - device = selectedDevice, - command = command, - onStart = { commands.value = getTextCommandUseCase() }, - onFailed = { commands.value = getTextCommandUseCase() }, - onComplete = { commands.value = getTextCommandUseCase() }, + private suspend fun updateSearchText(searchText: String) { + val commands = getTextCommandUseCase() + update { + copy( + searchText = searchText, + commands = commands.filter { it.title.startsWith(searchText) }, ) } } - fun sendInputText() { - val selectedDevice = state.value.selectedDevice ?: return - coroutineScope.launch { - sendUserInputTextCommandUseCase( - device = selectedDevice, - text = state.value.userInputText, - onStart = { isSending.value = true }, - onFailed = { isSending.value = false }, - onComplete = { isSending.value = false }, + private val ascii = (0..255).map { it.toChar() } + + private suspend fun updateTextCommandValue( + id: String, + value: String, + ) { + val isAscii = value.none { it !in ascii } + if (isAscii) { + textCommandRepository.updateTextCommandValue(id, value) + val commands = getTextCommandUseCase() + update { copy(commands = commands) } + } + } + + private suspend fun updateTextCommandTitle( + id: String, + value: String, + ) { + textCommandRepository.updateTextCommandTitle(id, value) + val commands = getTextCommandUseCase() + update { copy(commands = commands) } + } + + private suspend fun sendTextCommand() { + val selectedDevice = currentState.selectedDevice ?: return + val selectedCommand = currentState.selectedCommand ?: return + val selectedOption = currentState.selectedTextCommandOption + + executeTextCommandUseCase( + device = selectedDevice, + command = selectedCommand, + option = selectedOption, + onStart = { + val commands = getTextCommandUseCase() + update { copy(commands = commands) } + }, + onFailed = { + val commands = getTextCommandUseCase() + update { copy(commands = commands) } + }, + onComplete = { + val commands = getTextCommandUseCase() + update { copy(commands = commands) } + }, + ) + } + + private suspend fun addNewTextCommand() { + val command = + TextCommand( + title = "", + text = "", + ) + textCommandRepository.addTextCommand(command) + val commands = getTextCommandUseCase() + val commandIndex = commands.indexOf(command) + update { + copy( + commands = commands, + selectedCommandIndex = commandIndex, ) } } - fun sendTabCommand() { - val selectedDevice = state.value.selectedDevice ?: return - coroutineScope.launch { - sendTabCommandUseCase( - device = selectedDevice, - onStart = { isSendingTag.value = true }, - onFailed = { isSendingTag.value = false }, - onComplete = { isSendingTag.value = false }, + private suspend fun deleteInputText() { + val selectedCommand = currentState.selectedCommand ?: return + val selectedCommandIndex = currentState.commands.indexOf(selectedCommand) + textCommandRepository.removeTextCommand(selectedCommand) + + val commands = getTextCommandUseCase() + val newSelectedCommand = commands.getOrNull(selectedCommandIndex) + val newSelectedCommandIndex = if (newSelectedCommand == null) commands.lastIndex else selectedCommandIndex + update { + copy( + commands = commands, + selectedCommandIndex = newSelectedCommandIndex, ) } } - fun saveInputText() { - coroutineScope.launch { - addTextCommandUseCase(state.value.userInputText) - commands.value = getTextCommandUseCase() + private fun nextCommand() { + val nextIndex = currentState.commands.indexOf(currentState.selectedCommand) + 1 + if (0 <= nextIndex && nextIndex <= currentState.commands.lastIndex) { + update { copy(selectedCommandIndex = nextIndex) } } } - fun deleteInputText(command: TextCommand) { - coroutineScope.launch { - deleteTextCommandUseCase(command) - commands.value = getTextCommandUseCase() + private fun previousCommand() { + val previousIndex = currentState.commands.indexOf(currentState.selectedCommand) - 1 + if (0 <= previousIndex && previousIndex <= currentState.commands.lastIndex) { + update { copy(selectedCommandIndex = previousIndex) } } } + + private fun selectCommand(command: TextCommand) { + val index = currentState.commands.indexOf(command) + update { copy(selectedCommandIndex = index) } + } + + private fun updateTextCommandOption(value: TextCommand.Option) { + update { copy(selectedTextCommandOption = value) } + } } diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/InputTextActionMenu.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/InputTextActionMenu.kt deleted file mode 100644 index ccd8ff91..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/InputTextActionMenu.kt +++ /dev/null @@ -1,130 +0,0 @@ -package jp.kaleidot725.adbpad.ui.screen.text.component - -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardTab -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.Save -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import jp.kaleidot725.adbpad.ui.common.resource.defaultBorder -import jp.kaleidot725.adbpad.ui.component.RunningIndicator - -@Composable -fun InputTextActionMenu( - inputText: String, - onTextChange: (String) -> Unit, - onSend: () -> Unit, - isSending: Boolean, - canSendTab: Boolean, - onSendTab: () -> Unit, - isSendingTag: Boolean, - canSend: Boolean, - onSave: () -> Unit, - canSave: Boolean, - modifier: Modifier = Modifier, -) { - var text by remember { mutableStateOf(inputText) } - Row( - modifier = - modifier - .clip(RoundedCornerShape(4.dp)) - .defaultBorder() - .padding(horizontal = 12.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - BasicTextField( - value = text, - onValueChange = { - text = it - onTextChange(it) - }, - cursorBrush = SolidColor(MaterialTheme.colors.onBackground), - textStyle = TextStyle(color = MaterialTheme.colors.onBackground), - modifier = - Modifier - .weight(0.9f, true) - .align(Alignment.CenterVertically), - ) - - IconButton( - enabled = canSave, - onClick = { onSave() }, - modifier = Modifier.fillMaxHeight(), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = "Save", - ) - } - - IconButton( - enabled = canSendTab, - onClick = { onSendTab() }, - modifier = Modifier.fillMaxHeight(), - ) { - when (isSendingTag) { - true -> RunningIndicator(color = MaterialTheme.colors.primary) - else -> { - Icon( - imageVector = Icons.AutoMirrored.Default.KeyboardTab, - contentDescription = "Save", - ) - } - } - } - - IconButton( - enabled = canSend, - onClick = { onSend() }, - modifier = Modifier.fillMaxHeight(), - ) { - when (isSending) { - true -> RunningIndicator(color = MaterialTheme.colors.primary) - else -> { - Icon( - imageVector = Icons.AutoMirrored.Default.Send, - contentDescription = "Save", - ) - } - } - } - } -} - -@Preview -@Composable -private fun InputTextActionMenu_Preview() { - InputTextActionMenu( - inputText = "INPUT TEXT SAMPLE", - onSend = {}, - isSending = false, - canSend = true, - canSendTab = false, - onSendTab = {}, - onSave = {}, - canSave = true, - onTextChange = {}, - isSendingTag = false, - modifier = Modifier.height(50.dp), - ) -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandActions.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandActions.kt new file mode 100644 index 00000000..9f78171b --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandActions.kt @@ -0,0 +1,108 @@ +package jp.kaleidot725.adbpad.ui.screen.text.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import jp.kaleidot725.adbpad.domain.model.command.TextCommand +import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.common.dummy.TextCommandDummy + +@Composable +fun TextCommandActions( + command: TextCommand, + canSend: Boolean, + onSendText: () -> Unit, + selectedOption: TextCommand.Option, + onUpdateTextCommandOption: (TextCommand.Option) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Row( + modifier = modifier, + ) { + Spacer( + modifier = Modifier.weight(1.0f), + ) + + Box { + TextCommandButton( + selectedOption = selectedOption, + canSend = canSend, + isSending = command.isRunning, + onSend = onSendText, + onChangeOption = { expanded = true }, + modifier = Modifier, + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.width(250.dp), + ) { + TextCommand.Option.entries.forEach { option -> + DropdownMenuItem( + onClick = { + onUpdateTextCommandOption(option) + expanded = false + }, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Box(modifier = Modifier.size(20.dp).align(Alignment.CenterVertically)) { + if (selectedOption == option) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "", + modifier = Modifier.fillMaxSize(), + ) + } + } + + Text( + text = + when (option) { + TextCommand.Option.SendWithTab -> Language.textCommandOptionTab + TextCommand.Option.SendWithNewLine -> Language.textCommandOptionNewLine + }, + style = MaterialTheme.typography.subtitle2, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + } + } + } + } + } +} + +@Preview +@Composable +private fun Preview() { + TextCommandActions( + command = TextCommandDummy.value, + canSend = true, + onSendText = {}, + selectedOption = TextCommand.Option.SendWithTab, + onUpdateTextCommandOption = {}, + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandButton.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandButton.kt new file mode 100644 index 00000000..6be8c521 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandButton.kt @@ -0,0 +1,111 @@ +package jp.kaleidot725.adbpad.ui.screen.text.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import jp.kaleidot725.adbpad.domain.model.command.TextCommand +import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.component.RunningIndicator + +@Composable +fun TextCommandButton( + canSend: Boolean, + isSending: Boolean, + onSend: () -> Unit, + selectedOption: TextCommand.Option, + onChangeOption: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + Row( + modifier = + Modifier + .padding(8.dp) + .width(250.dp) + .height(35.dp) + .alpha(if (canSend) 1f else ContentAlpha.disabled) + .background(MaterialTheme.colors.primary, RoundedCornerShape(4.dp)), + ) { + Box( + modifier = + Modifier + .fillMaxHeight() + .weight(0.8f) + .clickable(enabled = canSend) { if (!isSending) onSend() }, + ) { + if (isSending) { + Box(Modifier.align(Alignment.Center)) { RunningIndicator() } + } else { + Text( + text = + when (selectedOption) { + TextCommand.Option.SendWithTab -> Language.textCommandOptionTab + TextCommand.Option.SendWithNewLine -> Language.textCommandOptionNewLine + }, + color = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.Center), + ) + } + } + + Spacer( + modifier = + Modifier + .fillMaxHeight() + .width(1.dp) + .background(Color.Black.copy(alpha = 0.6f)), + ) + + Box( + modifier = + Modifier + .fillMaxHeight() + .width(50.dp) + .background(Color.Black.copy(alpha = 0.3f)) + .clickable(enabled = canSend) { onChangeOption() }, + ) { + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "", + tint = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +@Preview +@Composable +private fun ScreenshotButton_Preview() { + MaterialTheme { + TextCommandButton( + selectedOption = TextCommand.Option.SendWithTab, + canSend = true, + isSending = false, + onSend = {}, + onChangeOption = {}, + modifier = Modifier, + ) + } +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandEditor.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandEditor.kt new file mode 100644 index 00000000..4bee55bf --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandEditor.kt @@ -0,0 +1,75 @@ +package jp.kaleidot725.adbpad.ui.screen.text.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import jp.kaleidot725.adbpad.domain.model.command.TextCommand +import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.common.dummy.TextCommandDummy +import jp.kaleidot725.adbpad.ui.common.resource.defaultBorder +import jp.kaleidot725.adbpad.ui.component.DefaultTextField + +@Composable +fun TextCommandEditor( + command: TextCommand, + onUpdateTitle: (id: String, value: String) -> Unit, + onUpdateText: (id: String, value: String) -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + DefaultTextField( + id = command.id, + initialText = command.title, + placeHolder = Language.textCommandUnTitle, + onUpdateText = { onUpdateTitle(command.id, it) }, + modifier = Modifier.weight(1.0f).height(48.dp).padding(horizontal = 12.dp), + ) + + IconButton( + onClick = onDelete, + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "", + ) + } + } + + Divider(modifier = Modifier.height(1.dp).fillMaxWidth().defaultBorder()) + + DefaultTextField( + id = command.id, + initialText = command.text, + placeHolder = "", + maxLines = Int.MAX_VALUE, + onUpdateText = { onUpdateText(command.id, it) }, + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 12.dp), + ) + } +} + +@Preview +@Composable +private fun Preview() { + TextCommandEditor( + command = TextCommandDummy.value, + onUpdateTitle = { _, _ -> }, + onUpdateText = { _, _ -> }, + onDelete = {}, + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandHeader.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandHeader.kt new file mode 100644 index 00000000..a2d22d10 --- /dev/null +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandHeader.kt @@ -0,0 +1,55 @@ +package jp.kaleidot725.adbpad.ui.screen.text.component + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.component.DefaultTextField + +@Composable +fun TextCommandHeader( + searchText: String, + onUpdateSearchText: (String) -> Unit, + onAddNewTextCommand: () -> Unit, + modifier: Modifier = Modifier, +) { + Row(modifier) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically).padding(12.dp), + ) + + DefaultTextField( + initialText = searchText, + onUpdateText = onUpdateSearchText, + placeHolder = Language.search, + modifier = Modifier.align(Alignment.CenterVertically).weight(1.0f), + ) + + IconButton( + onClick = onAddNewTextCommand, + ) { + Icon(imageVector = Icons.Default.Add, contentDescription = null) + } + } +} + +@Preview +@Composable +private fun Preview() { + TextCommandHeader( + searchText = "TEST", + onUpdateSearchText = {}, + onAddNewTextCommand = {}, + ) +} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandItem.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandItem.kt deleted file mode 100644 index cf241f79..00000000 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandItem.kt +++ /dev/null @@ -1,76 +0,0 @@ -package jp.kaleidot725.adbpad.ui.screen.text.component - -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import jp.kaleidot725.adbpad.domain.model.language.Language -import jp.kaleidot725.adbpad.ui.component.RunningIndicator - -@Composable -fun TextCommandItem( - text: String, - isRunning: Boolean, - onSend: () -> Unit, - canSend: Boolean, - onDelete: () -> Unit, - modifier: Modifier = Modifier, -) { - Card(modifier, elevation = 1.dp) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(8.dp), - ) { - Text( - text = text, - fontWeight = FontWeight.Bold, - maxLines = 2, - modifier = - Modifier - .fillMaxWidth() - .weight(weight = 0.8f, fill = true) - .align(Alignment.CenterVertically), - ) - Button( - onClick = { onDelete() }, - modifier = Modifier.align(Alignment.CenterVertically).width(85.dp), - ) { - Text(Language.delete) - } - Button( - onClick = { onSend() }, - enabled = canSend, - modifier = Modifier.align(Alignment.CenterVertically).width(85.dp), - ) { - when { - isRunning -> RunningIndicator() - else -> Text(text = Language.send) - } - } - } - } -} - -@Preview -@Composable -private fun TextCommandItem_Preview() { - TextCommandItem( - text = "あいうえお", - isRunning = false, - onSend = {}, - canSend = true, - onDelete = {}, - modifier = Modifier.fillMaxWidth().height(50.dp), - ) -} diff --git a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandList.kt b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandList.kt index eaeae6c7..cb7157b1 100644 --- a/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandList.kt +++ b/src/jvmMain/kotlin/jp/kaleidot725/adbpad/ui/screen/text/component/TextCommandList.kt @@ -2,46 +2,79 @@ package jp.kaleidot725.adbpad.ui.screen.text.component import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.dp import jp.kaleidot725.adbpad.domain.model.command.TextCommand import jp.kaleidot725.adbpad.domain.model.language.Language +import jp.kaleidot725.adbpad.ui.common.resource.clickableBackground @Composable fun TextCommandList( + selectedCommand: TextCommand?, commands: List, - onSend: (TextCommand) -> Unit, - canSend: Boolean, - onDelete: (TextCommand) -> Unit, + onSelectCommand: (TextCommand) -> Unit, + onNextCommand: () -> Unit, + onPreviousCommand: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { if (commands.isNotEmpty()) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.verticalScroll(rememberScrollState()), + val lazyColumnState = rememberLazyListState() + LazyColumn( + state = lazyColumnState, + modifier = + Modifier + .padding(4.dp) + .onKeyEvent { event -> + when { + event.key == Key.DirectionUp && event.type == KeyEventType.KeyDown -> { + onPreviousCommand() + true + } + event.key == Key.DirectionDown && event.type == KeyEventType.KeyDown -> { + onNextCommand() + true + } + else -> false + } + }, ) { - commands.forEach { command -> - TextCommandItem( - text = command.text, - isRunning = command.isRunning, - onSend = { onSend(command) }, - canSend = canSend, - onDelete = { onDelete(command) }, - modifier = Modifier.height(60.dp).fillMaxWidth().padding(2.dp), - ) + items( + items = commands, + ) { command -> + Row( + modifier = + Modifier + .fillMaxWidth() + .clickableBackground( + isSelected = selectedCommand?.id == command.id, + shape = RoundedCornerShape(4.dp), + ).clickable { onSelectCommand(command) } + .padding(horizontal = 12.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text(text = command.title.ifEmpty { Language.textCommandUnTitle }) + } } } } else { @@ -58,18 +91,20 @@ fun TextCommandList( private fun TextCommandList_Preview() { Column(horizontalAlignment = Alignment.CenterHorizontally) { TextCommandList( - commands = listOf(TextCommand("TEST1"), TextCommand("TEST2")), - onSend = {}, - canSend = true, - onDelete = {}, + selectedCommand = TextCommand(title = "Title", text = "Text"), + commands = listOf(TextCommand(title = "Title", text = "Text"), TextCommand(title = "Title", text = "Text")), + onSelectCommand = {}, + onNextCommand = {}, + onPreviousCommand = {}, modifier = Modifier.fillMaxWidth().weight(0.5f, true), ) TextCommandList( + selectedCommand = TextCommand(title = "Title", text = "Text"), commands = emptyList(), - onSend = {}, - canSend = true, - onDelete = {}, + onSelectCommand = {}, + onNextCommand = {}, + onPreviousCommand = {}, modifier = Modifier.fillMaxWidth().weight(0.5f, true).background(Color.LightGray), ) }