From 335ccae93d1e01c1fe44c008676466fb771d3f3e Mon Sep 17 00:00:00 2001 From: r0r-5chach Date: Mon, 28 Oct 2024 23:23:32 +0000 Subject: [PATCH] ui updates --- src/main/kotlin/Main.kt | 6 +- src/main/kotlin/processing/Microphone.kt | 13 +- .../kotlin/processing/SemanticSimilarity.kt | 25 +- src/main/kotlin/ui/Bars.kt | 39 +- src/main/kotlin/ui/Capture.kt | 45 +- src/main/kotlin/ui/Profiles.kt | 535 +++++++++++------- src/main/kotlin/ui/screens/Home.kt | 1 - src/main/kotlin/ui/screens/ProfileDetails.kt | 43 +- src/main/kotlin/ui/screens/ProfilesManager.kt | 62 +- src/main/kotlin/ui/screens/Settings.kt | 16 - src/main/resources/logback.xml | 3 +- 11 files changed, 446 insertions(+), 342 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index c67607b..f060191 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.window.application import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator import io.github.oshai.kotlinlogging.KotlinLogging +import processing.SemanticSimilarity import ui.screens.Home import ui.Bars import ui.Profiles.Companion.activeProfile @@ -32,7 +33,10 @@ fun main() = application { logger.info { "\n================== NEW APPLICATION SESSION ==================" } checkDirExists("logs/") checkDirExists("profiles/") - Window(onCloseRequest = ::exitApplication) { + + Window( + title = "M.I.A", + onCloseRequest = ::exitApplication) { MaterialTheme( colors = if (isSystemInDarkTheme()) darkColors() else lightColors() ) { diff --git a/src/main/kotlin/processing/Microphone.kt b/src/main/kotlin/processing/Microphone.kt index d156bb2..cee82c6 100644 --- a/src/main/kotlin/processing/Microphone.kt +++ b/src/main/kotlin/processing/Microphone.kt @@ -1,7 +1,10 @@ package processing +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.serialization.json.Json import javax.sound.sampled.AudioFormat @@ -28,12 +31,13 @@ class Microphone { */ @OptIn(DelicateCoroutinesApi::class) fun startCapture(captureDuration: Int) { - GlobalScope.async { + CoroutineScope(Dispatchers.IO + SupervisorJob()).async { + capturing = true + val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration val stt = STT() - captureLogger.info { "{NEW CAPTURE SESSION}" } - capturing = true + source.open(audioFormat, source.bufferSize) source.start() @@ -46,7 +50,6 @@ class Microphone { if (result.isNotEmpty()) { captureLogger.info { result } //TODO: Pass onto processing - } } stt.closeModel() @@ -56,9 +59,9 @@ class Microphone { * */ fun stopCapture() { + capturing = false source.stop() source.close() - capturing = false } /** * diff --git a/src/main/kotlin/processing/SemanticSimilarity.kt b/src/main/kotlin/processing/SemanticSimilarity.kt index 912b9e9..53a0112 100644 --- a/src/main/kotlin/processing/SemanticSimilarity.kt +++ b/src/main/kotlin/processing/SemanticSimilarity.kt @@ -25,7 +25,7 @@ class SemanticSimilarity { fun initTokenizer(): HuggingFaceTokenizer { logger.info { "Semantic similarity tokenizer initialized" } - return HuggingFaceTokenizer.newInstance("sentence-transformers/paraphrase-mpnet-base-v2") + return HuggingFaceTokenizer.newInstance("sentence-transformers/all-roberta-large-v1") } fun getCriteria(): Criteria = Criteria.builder() @@ -50,19 +50,30 @@ class SemanticSimilarity { fun generateEmbedding(input: String): NDArray { - val inputList = tokenizer.encode(input).toNDList(manager, false) + val inputList = tokenizer.encode(input).toNDList(manager, true) val inputIds = inputList[0].expandDims(0) val attentionMask = inputList[1].expandDims(0) - return model.predict( + val tokenEmbeddings = model.predict( NDList(inputIds, attentionMask) - )[0].mean(intArrayOf(1)).normalize(2.0, 1) + )[0] + + return meanPooling(tokenEmbeddings, attentionMask) + } + + fun meanPooling(tokenEmbeddings: NDArray, attentionMask: NDArray): NDArray { + val inputMask = attentionMask.expandDims(-1).toType(tokenEmbeddings.dataType, false) // Shape: [1, seq_len, 1] + val sumEmbeddings = tokenEmbeddings.mul(inputMask).sum(intArrayOf(1)) // Sum over seq_len + val sumMask = inputMask.sum(intArrayOf(1)).clip(1e-9f, Float.MAX_VALUE) // Prevent division by zero + + return sumEmbeddings.div(sumMask) } fun cosineSimilarity(input: NDArray, command: NDArray): Float { - val inputVec = input.squeeze(0) - val commandVec = command.squeeze(0) - val dotProduct = input.matMul(commandVec).getFloat() + val inputVec = input.squeeze() + val commandVec = command.squeeze() + + val dotProduct = inputVec.matMul(commandVec).getFloat() val normInput = inputVec.norm().getFloat() val normCommand = commandVec.norm().getFloat() diff --git a/src/main/kotlin/ui/Bars.kt b/src/main/kotlin/ui/Bars.kt index a1457b3..d19a0ce 100644 --- a/src/main/kotlin/ui/Bars.kt +++ b/src/main/kotlin/ui/Bars.kt @@ -2,33 +2,22 @@ package ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.ConstraintLayoutScope -import androidx.constraintlayout.compose.Dimension import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import transparentButton +import ui.Profiles.Companion.Buttons.Companion.createProfile +import ui.Profiles.Companion.Buttons.Companion.currentProfile import ui.screens.ProfilesManager import ui.screens.Settings @@ -68,19 +57,19 @@ class Bars { ) { val (currentProfile, createProfile) = createRefs() - Profiles.currentProfile(Modifier.constrainAs(currentProfile) { + currentProfile(Modifier.constrainAs(currentProfile) { linkTo(parent.top, parent.bottom) start.linkTo(parent.start) }) if (LocalNavigator.currentOrThrow.lastItem is ProfilesManager) { - Profiles.createProfile(Modifier.constrainAs(createProfile) { + createProfile(Modifier.constrainAs(createProfile) { linkTo(parent.top, parent.bottom) end.linkTo(parent.end) }) } } - }p + } @Composable fun backButton(modifier: Modifier) { val navigator = LocalNavigator.currentOrThrow @@ -97,5 +86,21 @@ class Bars { ) } } + + @Composable + fun settingsButton(modifier: Modifier) { + val navigator = LocalNavigator.currentOrThrow + + transparentButton( + modifier = modifier, + icon = Icons.Filled.Settings, + contentDescription = "Settings", + onClick = { + if (navigator.lastItem !is Settings) { + navigator.push(Settings()) + } + }, + ) + } } } \ No newline at end of file diff --git a/src/main/kotlin/ui/Capture.kt b/src/main/kotlin/ui/Capture.kt index 3ea14a0..caaa719 100644 --- a/src/main/kotlin/ui/Capture.kt +++ b/src/main/kotlin/ui/Capture.kt @@ -8,15 +8,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalDrawer import androidx.compose.material.Slider import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -31,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -43,6 +37,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.delay import logger import transparentButton +import ui.Bars.Companion.settingsButton import ui.screens.Settings import ui.screens.Settings.Companion.userSettings import java.io.File @@ -52,13 +47,14 @@ import kotlin.math.roundToInt class Capture { companion object { + val mic = Microphone() val captureLogger = KotlinLogging.logger("capture_logger") val logUpdateDelay = (getCaptureDuration().toLong() * 1000) + 100 // captureDuration + 0.1s in ms fun getCaptureDuration() = userSettings.getProperty("capture_duration").toInt() - fun getCaptureOnClick(mic: Microphone) = if (!mic.capturing) mic.startCapture(getCaptureDuration()) else mic.stopCapture() - fun getCaptureIcon(capturing: Boolean) = if (!capturing) Icons.Filled.PlayArrow else Icons.Filled.Stop - fun getCaptureDescription(capturing: Boolean) = if (!capturing) "Start" else "Stop" + fun getCaptureOnClick(mic: Microphone) = if (mic.capturing) mic.stopCapture() else mic.startCapture(getCaptureDuration()) + fun getCaptureIcon(capturing: Boolean) = if (capturing) Icons.Filled.Stop else Icons.Filled.PlayArrow + fun getCaptureDescription(capturing: Boolean) = if (capturing) "Stop" else "Start" fun getCaptureLog() = File(Paths.get("logs/capture.log").absolutePathString()).readLines().toMutableList() @Composable @@ -79,7 +75,7 @@ class Capture { linkTo(button.end, settings.start, startMargin = 5.dp) }) - Settings.settingsButton(Modifier.constrainAs(settings) { + settingsButton(Modifier.constrainAs(settings) { linkTo(parent.top, parent.bottom) linkTo(info.end, parent.end, startMargin = 5.dp, endMargin = 5.dp) }) @@ -89,8 +85,7 @@ class Capture { @OptIn(DelicateCoroutinesApi::class) @Composable fun captureButton(modifier: Modifier) { - val mic = Microphone() - var isCapturing by rememberSaveable { mutableStateOf(mic.capturing) } + var isCapturing by remember { mutableStateOf(mic.capturing) } transparentButton( modifier = modifier, @@ -139,20 +134,20 @@ class Capture { @Composable fun captureLog() { var lines: MutableState> = rememberSaveable { mutableStateOf(getCaptureLog())} + SelectionContainer { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 25.dp) + .padding(top = 10.dp, bottom = 50.dp) + .verticalScroll(rememberScrollState()) + ) { + lines.value.add(0, "Capture Log:") - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 25.dp) - .padding(top = 10.dp, bottom = 50.dp) - .verticalScroll(rememberScrollState()) - ) { - var count = 0 - lines.value.add(0, "Capture Log:") - - SelectionContainer { + var count = 0 lines.value.forEach { line -> - val backgroundColor = if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background + val backgroundColor = + if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background Text( modifier = Modifier .background(backgroundColor, CircleShape) @@ -161,7 +156,7 @@ class Capture { color = MaterialTheme.colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, - text = line, + text = line ) count++ } diff --git a/src/main/kotlin/ui/Profiles.kt b/src/main/kotlin/ui/Profiles.kt index 55302b5..7f14e41 100644 --- a/src/main/kotlin/ui/Profiles.kt +++ b/src/main/kotlin/ui/Profiles.kt @@ -1,11 +1,14 @@ package ui +import ai.djl.ndarray.NDArray import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +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.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -19,24 +22,19 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Create +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Save import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.fasterxml.jackson.core.JsonEncoding -import kotlinx.coroutines.delay import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -45,23 +43,27 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import transparentButton -import ui.Capture.Companion.getCaptureDuration +import ui.Profiles.Companion.Dialogs.Companion.activateDialog +import ui.Profiles.Companion.Dialogs.Companion.deleteDialog +import ui.Profiles.Companion.Entities.Profile import ui.screens.ProfileDetails import ui.screens.ProfilesManager -import ui.screens.ProfilesManager.Companion.profileListItem import ui.screens.Settings import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream -import java.nio.file.Path import java.nio.file.Paths +import java.util.UUID import kotlin.io.path.absolutePathString class Profiles { companion object { val activeProfile = mutableStateOf(getCurrentProfile()) val profiles = mutableStateOf(getProfiles()) + val showActivateDialog = mutableStateOf(false) + val showDeleteDialog = mutableStateOf(false) + val selectedProfile = mutableStateOf(Profile()) fun getCurrentProfile() = load(Settings.userSettings.getProperty("current_profile")) fun getProfileLocation(profile: String?) = Paths.get("profiles/$profile.profile").absolutePathString() @@ -77,191 +79,6 @@ class Profiles { } as MutableList } - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun profileList() { - var showActivateDialog by rememberSaveable { mutableStateOf(false) } - var showDeleteDialog by rememberSaveable { mutableStateOf(false) } - var selectedProfile by rememberSaveable { mutableStateOf(Profile()) } - - val navigator = LocalNavigator.currentOrThrow - profiles.value = getProfiles() - - Column( - modifier = Modifier - .padding(horizontal = 25.dp) - .padding(top = 10.dp, bottom = 50.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.End, - ) { - - profileListItem( - modifier = Modifier - .background(MaterialTheme.colors.onBackground.copy(alpha = 0.05f), CircleShape), - profileName = "Profile Name", - programName = "Program Name", - aiName = "AI Name" - ) - - var count = 1 - profiles.value.forEach { profile -> - val backgroundColor = if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background - - profileListItem( - modifier = Modifier - .background(backgroundColor, CircleShape) - .combinedClickable( - onClick = { - navigator.push(ProfileDetails(profile)) - }, - onDoubleClick = { - selectedProfile = profile - showActivateDialog = true - }, - onLongClick = { - selectedProfile = profile - showDeleteDialog = true - }, - ), - profileName = profile.name, - programName = profile.programName, - aiName = profile.aiName, - ) - count++ - } - - if (showActivateDialog) { - profileDialog( - title = "Confirm Activation", - text = "Activate ${selectedProfile.name}?", - confirmClick = { - activeProfile.value = selectedProfile - Settings.userSettings.setProperty("current_profile", selectedProfile.name) - Settings.userSettings.save() - showActivateDialog = false - }, - cancelClick = { - showActivateDialog = false - }, - ) - } - if (showDeleteDialog) { - profileDialog( - title = "Confirm Deletion", - text = "Delete ${selectedProfile.name}?", - confirmClick = { - File(getProfileLocation(selectedProfile.name)).delete() - - if (activeProfile.value == selectedProfile) { - Settings.userSettings.remove("current_profile") - Settings.userSettings.save() - activeProfile.value = getCurrentProfile() - } - showDeleteDialog = false - }, - cancelClick = { - showDeleteDialog = false - } - ) - } - } - - } - - @Composable - fun profileDialog(title: String, text: String, confirmClick: () -> Unit, cancelClick: () -> Unit) { - AlertDialog( - title = { Text(text = title) }, - text = { Text(text = text) }, - onDismissRequest = { - cancelClick - }, - confirmButton = { - Button( - onClick = confirmClick - ) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = "Accept" - ) - } - }, - dismissButton = { - Button( - onClick = cancelClick - ) { - Icon( - imageVector = Icons.Filled.Cancel, - contentDescription = "Cancel" - ) - } - } - ) - } - - @Composable - fun createProfile(modifier: Modifier) { - val navigator = LocalNavigator.currentOrThrow - - Box( - modifier = modifier - .background(MaterialTheme.colors.primary, CircleShape) - ) { - transparentButton( - modifier = Modifier, - icon = Icons.Filled.Create, - contentDescription = "Create new profile", - onClick = { - navigator.push(ProfileDetails(null)) - }, - ) - } - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun currentProfile(modifier: Modifier) { - val navigator = LocalNavigator.currentOrThrow - - Text( - modifier = modifier - .background(MaterialTheme.colors.primary, CircleShape) - .padding(5.dp) - .width(150.dp) - .combinedClickable( - onClick = { navigator.push(ProfileDetails(activeProfile.value)) }, - onDoubleClick = { - if (navigator.lastItem !is ProfilesManager) { - navigator.push(ProfilesManager()) - } - } - ), - color = MaterialTheme.colors.onPrimary, - textAlign = TextAlign.Center, - text = activeProfile.value.name - ) - } - - @Serializable - data class Profile( - val name: String = "{NO NAME}", - @SerialName("ai_name") - val aiName: String = "{NO NAME}", - @SerialName("program_name") - val programName: String = "{NO NAME}", - @SerialName("program_path") - val programPath: String = "{NO NAME}", - @SerialName("commands") - val commands: List? = null, - ) - - @Serializable - data class Command( - @SerialName("wording_Variants") - val wordingVariants: List? = null, - val triggers: List? = null - ) - @OptIn(ExperimentalSerializationApi::class) fun load(profile: File?): Profile { var file: Profile @@ -270,11 +87,9 @@ class Profiles { val stream = FileInputStream(profile!!) file = Json.decodeFromStream(stream).copy(name = profile.nameWithoutExtension) stream.close() - } - catch (ne: FileNotFoundException) { + } catch (ne: FileNotFoundException) { file = Profile(name = "Not Selected") - } - catch (je: SerializationException) { + } catch (je: SerializationException) { file = Profile(name = profile!!.nameWithoutExtension) } @@ -291,6 +106,324 @@ class Profiles { stream.flush() stream.close() } + /** + * + */ + class Entities { + @Serializable + data class Profile( + var name: String = "{NO NAME}", + @SerialName("ai_name") + var aiName: String = "{NO NAME}", + @SerialName("program_name") + var programName: String = "{NO NAME}", + @SerialName("program_path") + var programPath: String = "{NO NAME}", + @SerialName("commands") + var commands: List? = null + ) + @Serializable + data class Command( + @SerialName("wording_Variants") + val wordingVariants: List? = null, + val triggers: List? = null + ) + + @Serializable + data class WordingVariant( + val wording: String, + val embedding: NDArray? = null + ) + } + /** + * + */ + class Dialogs { + companion object { + @Composable + fun editDialog(flag: MutableState, title: String, text: String, confirmClick: () -> Unit) { + if (flag.value) { + profileDialog( + title = title, + text = text, + confirmClick = confirmClick, + cancelClick = { + flag.value = false + } + ) + } + } + + @Composable + fun activateDialog(profile: Profile) { + editDialog( + title = "Confirm Activation", + text = "Activate ${profile.name}?", + flag = showActivateDialog, + confirmClick = { + activeProfile.value = profile + Settings.userSettings.setProperty("current_profile", profile.name) + Settings.userSettings.save() + showActivateDialog.value = false + } + ) + + } + + @Composable + fun deleteDialog(profile: Profile) { + val navigator = LocalNavigator.currentOrThrow + + editDialog( + title = "Confirm Deletion", + text = "Delete ${profile.name}?", + flag = showDeleteDialog, + confirmClick = { + File(getProfileLocation(profile.name)).delete() + profiles.value = getProfiles() + + if (activeProfile.value == profile) { + Settings.userSettings.remove("current_profile") + Settings.userSettings.save() + activeProfile.value = getCurrentProfile() + } + + + showDeleteDialog.value = false + + if (navigator.lastItem is ProfileDetails) { + navigator.pop() + } + } + ) + } + + @Composable + fun profileDialog(title: String, text: String, confirmClick: () -> Unit, cancelClick: () -> Unit) { + AlertDialog( + title = { Text(text = title) }, + text = { Text(text = text) }, + onDismissRequest = { + cancelClick + }, + confirmButton = { + Button( + onClick = confirmClick + ) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = "Accept" + ) + } + }, + dismissButton = { + Button( + onClick = cancelClick + ) { + Icon( + imageVector = Icons.Filled.Cancel, + contentDescription = "Cancel" + ) + } + } + ) + } + } + } + /** + * + */ + class Lists { + companion object { + @Composable + fun profileList() { + profiles.value = getProfiles() + + Column( + modifier = Modifier + .padding(horizontal = 25.dp) + .padding(top = 10.dp, bottom = 50.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.End, + ) { + profileListHeader() + profileListEntries() + + activateDialog(selectedProfile.value) + deleteDialog(selectedProfile.value) + } + + } + + @Composable + fun profileListItem(profileName: String, programName: String, aiName: String, modifier: Modifier) { + + Row( + modifier = modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + + listOf(profileName, programName, aiName).forEach { name -> + Text( + modifier = Modifier + .weight(1f), + text = name, + textAlign = TextAlign.Center + ) + } + } + + + } + + @Composable + fun profileListHeader() { + profileListItem( + modifier = Modifier + .background(MaterialTheme.colors.onBackground.copy(alpha = 0.05f), CircleShape), + profileName = "Profile Name", + programName = "Program Name", + aiName = "AI Name" + ) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun profileListEntries() { + val navigator = LocalNavigator.currentOrThrow + var count = 1 + + profiles.value.forEach { profile -> + val backgroundColor = + if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background + + profileListItem( + modifier = Modifier + .background(backgroundColor, CircleShape) + .combinedClickable( + onClick = { + navigator.push(ProfileDetails(profile)) + }, + onDoubleClick = { + selectedProfile.value = profile + showActivateDialog.value = true + }, + onLongClick = { + selectedProfile.value = profile + showDeleteDialog.value = true + }, + ), + profileName = profile.name, + programName = profile.programName, + aiName = profile.aiName, + ) + count++ + } + } + } + } + /** + * + */ + class Buttons { + companion object { + @Composable + fun createProfile(modifier: Modifier) { + val navigator = LocalNavigator.currentOrThrow + + Box( + modifier = modifier + .background(MaterialTheme.colors.primary, CircleShape) + ) { + transparentButton( + modifier = Modifier, + icon = Icons.Filled.Create, + contentDescription = "Create new profile", + onClick = { + navigator.push(ProfileDetails()) + }, + ) + } + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun currentProfile(modifier: Modifier) { + val navigator = LocalNavigator.currentOrThrow + + Text( + modifier = modifier + .background(MaterialTheme.colors.primary, CircleShape) + .padding(5.dp) + .width(150.dp) + .combinedClickable( + onClick = { navigator.push(ProfileDetails(activeProfile.value)) }, + onDoubleClick = { + if (navigator.lastItem !is ProfilesManager) { + navigator.push(ProfilesManager()) + } + } + ), + color = MaterialTheme.colors.onPrimary, + textAlign = TextAlign.Center, + text = activeProfile.value.name + ) + } + + @Composable + fun saveButton(profile: Profile) { + val navigator = LocalNavigator.currentOrThrow + + Button( + onClick = { + if (profile.name == "{NO NAME}") { + profile.name = UUID.randomUUID().toString() + } + save(profile) + navigator.pop() + } + ) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = "Save Profile" + ) + } + } + + @Composable + fun deleteButton(profile: Profile) { + Button( + onClick = { + val file = File(getProfileLocation(profile.name)) + + if (file.exists()) { + showDeleteDialog.value = true + } + } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = "Delete Profile" + ) + } + } + + @Composable + fun activateButton(profile: Profile) { + Button( + onClick = { + showActivateDialog.value = true + } + ) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Activate Profile" + ) + } + } + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/ui/screens/Home.kt b/src/main/kotlin/ui/screens/Home.kt index 141f611..9e17999 100644 --- a/src/main/kotlin/ui/screens/Home.kt +++ b/src/main/kotlin/ui/screens/Home.kt @@ -12,5 +12,4 @@ class Home: Screen { override fun Content() { Capture.captureLog() } - } \ No newline at end of file diff --git a/src/main/kotlin/ui/screens/ProfileDetails.kt b/src/main/kotlin/ui/screens/ProfileDetails.kt index a8ed858..883d2c6 100644 --- a/src/main/kotlin/ui/screens/ProfileDetails.kt +++ b/src/main/kotlin/ui/screens/ProfileDetails.kt @@ -1,15 +1,44 @@ package ui.screens +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text import androidx.compose.runtime.Composable import cafe.adriel.voyager.core.screen.Screen -import ui.Profiles.Companion.Profile +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import ui.Profiles.Companion.Dialogs.Companion.activateDialog +import ui.Profiles.Companion.Buttons.Companion.activateButton +import ui.Profiles.Companion.Buttons.Companion.saveButton +import ui.Profiles.Companion.Buttons.Companion.deleteButton +import ui.Profiles.Companion.Dialogs.Companion.deleteDialog +import ui.Profiles.Companion.Entities.Profile -class ProfileDetails(profile: Profile?): Screen { +class ProfileDetails(val profile: Profile): Screen { + /** + * + */ + constructor(): this(Profile()) + /** + * + */ @Composable - override fun Content() { - //TODO: Display all fields for profile - // Allow editing - // Save Profile Button - // Delete Profile Button + override fun Content() { //TODO: Description of what each attribute represents + Column { + Text("Name: ${profile.name}") //TODO: Text Field + Text("AI name: ${profile.aiName}") //TODO: Text Field + Text("Program name: ${profile.programName}") //TODO: Text Field + Text("Program path: ${profile.programPath}") //TODO: FileDialog + + Row { + activateButton(profile) + saveButton(profile) + deleteButton(profile) + activateDialog(profile) + deleteDialog(profile) + } + } + //TODO: Display commands + // Allow editing. on unFocus, update profile entry } } \ No newline at end of file diff --git a/src/main/kotlin/ui/screens/ProfilesManager.kt b/src/main/kotlin/ui/screens/ProfilesManager.kt index 6450fa4..c142216 100644 --- a/src/main/kotlin/ui/screens/ProfilesManager.kt +++ b/src/main/kotlin/ui/screens/ProfilesManager.kt @@ -1,70 +1,12 @@ package ui.screens -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Create -import androidx.compose.material.rememberBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.LineHeightStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.window.Dialog -import androidx.constraintlayout.compose.ConstraintLayout import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import ui.Profiles -import ui.Profiles.Companion.Profile +import ui.Profiles.Companion.Lists.Companion.profileList class ProfilesManager(): Screen { - @Composable override fun Content() { - Profiles.profileList() - } - - companion object { - @Composable - fun profileListItem(profileName: String, programName: String, aiName: String, modifier: Modifier) { - - Row( - modifier = modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - - listOf(profileName, programName, aiName).forEach { name -> - Text( - modifier = Modifier - .weight(1f), - text = name, - textAlign = TextAlign.Center - ) - } - } - - - } + profileList() } } \ No newline at end of file diff --git a/src/main/kotlin/ui/screens/Settings.kt b/src/main/kotlin/ui/screens/Settings.kt index 175898b..2dcbf80 100644 --- a/src/main/kotlin/ui/screens/Settings.kt +++ b/src/main/kotlin/ui/screens/Settings.kt @@ -45,22 +45,6 @@ class Settings: Screen { companion object { val userSettings = UserSettings() - @Composable - fun settingsButton(modifier: Modifier) { - val navigator = LocalNavigator.currentOrThrow - - transparentButton( - modifier = modifier, - icon = Icons.Filled.Settings, - contentDescription = "Settings", - onClick = { - if (navigator.lastItem !is Settings) { - navigator.push(Settings()) - } - }, - ) - } - @Composable fun setting(setting: Map.Entry) { when(setting.key) { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index ad094db..909487c 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,12 +1,11 @@ - - +