ui updates

This commit is contained in:
Joshua Perry 2024-10-28 23:23:32 +00:00
parent 10f17f07ab
commit 335ccae93d
11 changed files with 446 additions and 342 deletions

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.window.application
import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import processing.SemanticSimilarity
import ui.screens.Home import ui.screens.Home
import ui.Bars import ui.Bars
import ui.Profiles.Companion.activeProfile import ui.Profiles.Companion.activeProfile
@ -32,7 +33,10 @@ fun main() = application {
logger.info { "\n================== NEW APPLICATION SESSION ==================" } logger.info { "\n================== NEW APPLICATION SESSION ==================" }
checkDirExists("logs/") checkDirExists("logs/")
checkDirExists("profiles/") checkDirExists("profiles/")
Window(onCloseRequest = ::exitApplication) {
Window(
title = "M.I.A",
onCloseRequest = ::exitApplication) {
MaterialTheme( MaterialTheme(
colors = if (isSystemInDarkTheme()) darkColors() else lightColors() colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
) { ) {

View File

@ -1,7 +1,10 @@
package processing package processing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import javax.sound.sampled.AudioFormat import javax.sound.sampled.AudioFormat
@ -28,12 +31,13 @@ class Microphone {
*/ */
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun startCapture(captureDuration: Int) { fun startCapture(captureDuration: Int) {
GlobalScope.async { CoroutineScope(Dispatchers.IO + SupervisorJob()).async {
capturing = true
val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration
val stt = STT() val stt = STT()
captureLogger.info { "{NEW CAPTURE SESSION}" } captureLogger.info { "{NEW CAPTURE SESSION}" }
capturing = true
source.open(audioFormat, source.bufferSize) source.open(audioFormat, source.bufferSize)
source.start() source.start()
@ -46,7 +50,6 @@ class Microphone {
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
captureLogger.info { result } captureLogger.info { result }
//TODO: Pass onto processing //TODO: Pass onto processing
} }
} }
stt.closeModel() stt.closeModel()
@ -56,9 +59,9 @@ class Microphone {
* *
*/ */
fun stopCapture() { fun stopCapture() {
capturing = false
source.stop() source.stop()
source.close() source.close()
capturing = false
} }
/** /**
* *

View File

@ -25,7 +25,7 @@ class SemanticSimilarity {
fun initTokenizer(): HuggingFaceTokenizer { fun initTokenizer(): HuggingFaceTokenizer {
logger.info { "Semantic similarity tokenizer initialized" } 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<NDList, NDList> = Criteria.builder() fun getCriteria(): Criteria<NDList, NDList> = Criteria.builder()
@ -50,19 +50,30 @@ class SemanticSimilarity {
fun generateEmbedding(input: String): NDArray { 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 inputIds = inputList[0].expandDims(0)
val attentionMask = inputList[1].expandDims(0) val attentionMask = inputList[1].expandDims(0)
return model.predict( val tokenEmbeddings = model.predict(
NDList(inputIds, attentionMask) 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 { fun cosineSimilarity(input: NDArray, command: NDArray): Float {
val inputVec = input.squeeze(0) val inputVec = input.squeeze()
val commandVec = command.squeeze(0) val commandVec = command.squeeze()
val dotProduct = input.matMul(commandVec).getFloat()
val dotProduct = inputVec.matMul(commandVec).getFloat()
val normInput = inputVec.norm().getFloat() val normInput = inputVec.norm().getFloat()
val normCommand = commandVec.norm().getFloat() val normCommand = commandVec.norm().getFloat()

View File

@ -2,33 +2,22 @@ package ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box 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.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.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.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout 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.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import transparentButton import transparentButton
import ui.Profiles.Companion.Buttons.Companion.createProfile
import ui.Profiles.Companion.Buttons.Companion.currentProfile
import ui.screens.ProfilesManager import ui.screens.ProfilesManager
import ui.screens.Settings import ui.screens.Settings
@ -68,19 +57,19 @@ class Bars {
) { ) {
val (currentProfile, createProfile) = createRefs() val (currentProfile, createProfile) = createRefs()
Profiles.currentProfile(Modifier.constrainAs(currentProfile) { currentProfile(Modifier.constrainAs(currentProfile) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}) })
if (LocalNavigator.currentOrThrow.lastItem is ProfilesManager) { if (LocalNavigator.currentOrThrow.lastItem is ProfilesManager) {
Profiles.createProfile(Modifier.constrainAs(createProfile) { createProfile(Modifier.constrainAs(createProfile) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
end.linkTo(parent.end) end.linkTo(parent.end)
}) })
} }
} }
}p }
@Composable @Composable
fun backButton(modifier: Modifier) { fun backButton(modifier: Modifier) {
val navigator = LocalNavigator.currentOrThrow 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())
}
},
)
}
} }
} }

View File

@ -8,15 +8,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll 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.MaterialTheme
import androidx.compose.material.ModalDrawer
import androidx.compose.material.Slider import androidx.compose.material.Slider
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -43,6 +37,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import logger import logger
import transparentButton import transparentButton
import ui.Bars.Companion.settingsButton
import ui.screens.Settings import ui.screens.Settings
import ui.screens.Settings.Companion.userSettings import ui.screens.Settings.Companion.userSettings
import java.io.File import java.io.File
@ -52,13 +47,14 @@ import kotlin.math.roundToInt
class Capture { class Capture {
companion object { companion object {
val mic = Microphone()
val captureLogger = KotlinLogging.logger("capture_logger") val captureLogger = KotlinLogging.logger("capture_logger")
val logUpdateDelay = (getCaptureDuration().toLong() * 1000) + 100 // captureDuration + 0.1s in ms val logUpdateDelay = (getCaptureDuration().toLong() * 1000) + 100 // captureDuration + 0.1s in ms
fun getCaptureDuration() = userSettings.getProperty("capture_duration").toInt() fun getCaptureDuration() = userSettings.getProperty("capture_duration").toInt()
fun getCaptureOnClick(mic: Microphone) = if (!mic.capturing) mic.startCapture(getCaptureDuration()) else mic.stopCapture() fun getCaptureOnClick(mic: Microphone) = if (mic.capturing) mic.stopCapture() else mic.startCapture(getCaptureDuration())
fun getCaptureIcon(capturing: Boolean) = if (!capturing) Icons.Filled.PlayArrow else Icons.Filled.Stop fun getCaptureIcon(capturing: Boolean) = if (capturing) Icons.Filled.Stop else Icons.Filled.PlayArrow
fun getCaptureDescription(capturing: Boolean) = if (!capturing) "Start" else "Stop" fun getCaptureDescription(capturing: Boolean) = if (capturing) "Stop" else "Start"
fun getCaptureLog() = File(Paths.get("logs/capture.log").absolutePathString()).readLines().toMutableList() fun getCaptureLog() = File(Paths.get("logs/capture.log").absolutePathString()).readLines().toMutableList()
@Composable @Composable
@ -79,7 +75,7 @@ class Capture {
linkTo(button.end, settings.start, startMargin = 5.dp) linkTo(button.end, settings.start, startMargin = 5.dp)
}) })
Settings.settingsButton(Modifier.constrainAs(settings) { settingsButton(Modifier.constrainAs(settings) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
linkTo(info.end, parent.end, startMargin = 5.dp, endMargin = 5.dp) linkTo(info.end, parent.end, startMargin = 5.dp, endMargin = 5.dp)
}) })
@ -89,8 +85,7 @@ class Capture {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@Composable @Composable
fun captureButton(modifier: Modifier) { fun captureButton(modifier: Modifier) {
val mic = Microphone() var isCapturing by remember { mutableStateOf(mic.capturing) }
var isCapturing by rememberSaveable { mutableStateOf(mic.capturing) }
transparentButton( transparentButton(
modifier = modifier, modifier = modifier,
@ -139,20 +134,20 @@ class Capture {
@Composable @Composable
fun captureLog() { fun captureLog() {
var lines: MutableState<MutableList<String>> = rememberSaveable { mutableStateOf(getCaptureLog())} var lines: MutableState<MutableList<String>> = 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( var count = 0
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 {
lines.value.forEach { line -> 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( Text(
modifier = Modifier modifier = Modifier
.background(backgroundColor, CircleShape) .background(backgroundColor, CircleShape)
@ -161,7 +156,7 @@ class Capture {
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
text = line, text = line
) )
count++ count++
} }

View File

@ -1,11 +1,14 @@
package ui package ui
import ai.djl.ndarray.NDArray
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState 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.Icons
import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Check 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.Create
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Save
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.fasterxml.jackson.core.JsonEncoding
import kotlinx.coroutines.delay
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -45,23 +43,27 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream import kotlinx.serialization.json.encodeToStream
import transparentButton 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.ProfileDetails
import ui.screens.ProfilesManager import ui.screens.ProfilesManager
import ui.screens.ProfilesManager.Companion.profileListItem
import ui.screens.Settings import ui.screens.Settings
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.UUID
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
class Profiles { class Profiles {
companion object { companion object {
val activeProfile = mutableStateOf(getCurrentProfile()) val activeProfile = mutableStateOf(getCurrentProfile())
val profiles = mutableStateOf(getProfiles()) 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 getCurrentProfile() = load(Settings.userSettings.getProperty("current_profile"))
fun getProfileLocation(profile: String?) = Paths.get("profiles/$profile.profile").absolutePathString() fun getProfileLocation(profile: String?) = Paths.get("profiles/$profile.profile").absolutePathString()
@ -77,191 +79,6 @@ class Profiles {
} as MutableList<Profile> } as MutableList<Profile>
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun profileList() {
var showActivateDialog by rememberSaveable { mutableStateOf(false) }
var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
var selectedProfile by rememberSaveable { mutableStateOf<Profile>(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<Command>? = null,
)
@Serializable
data class Command(
@SerialName("wording_Variants")
val wordingVariants: List<String>? = null,
val triggers: List<String>? = null
)
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun load(profile: File?): Profile { fun load(profile: File?): Profile {
var file: Profile var file: Profile
@ -270,11 +87,9 @@ class Profiles {
val stream = FileInputStream(profile!!) val stream = FileInputStream(profile!!)
file = Json.decodeFromStream<Profile>(stream).copy(name = profile.nameWithoutExtension) file = Json.decodeFromStream<Profile>(stream).copy(name = profile.nameWithoutExtension)
stream.close() stream.close()
} } catch (ne: FileNotFoundException) {
catch (ne: FileNotFoundException) {
file = Profile(name = "Not Selected") file = Profile(name = "Not Selected")
} } catch (je: SerializationException) {
catch (je: SerializationException) {
file = Profile(name = profile!!.nameWithoutExtension) file = Profile(name = profile!!.nameWithoutExtension)
} }
@ -291,6 +106,324 @@ class Profiles {
stream.flush() stream.flush()
stream.close() 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<Command>? = null
)
@Serializable
data class Command(
@SerialName("wording_Variants")
val wordingVariants: List<NDArray>? = null,
val triggers: List<String>? = null
)
@Serializable
data class WordingVariant(
val wording: String,
val embedding: NDArray? = null
)
}
/**
*
*/
class Dialogs {
companion object {
@Composable
fun editDialog(flag: MutableState<Boolean>, 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"
)
}
}
}
}
} }
} }

View File

@ -12,5 +12,4 @@ class Home: Screen {
override fun Content() { override fun Content() {
Capture.captureLog() Capture.captureLog()
} }
} }

View File

@ -1,15 +1,44 @@
package ui.screens 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 androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen 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 @Composable
override fun Content() { override fun Content() { //TODO: Description of what each attribute represents
//TODO: Display all fields for profile Column {
// Allow editing Text("Name: ${profile.name}") //TODO: Text Field
// Save Profile Button Text("AI name: ${profile.aiName}") //TODO: Text Field
// Delete Profile Button 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
} }
} }

View File

@ -1,70 +1,12 @@
package ui.screens 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.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.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import ui.Profiles.Companion.Lists.Companion.profileList
import cafe.adriel.voyager.navigator.currentOrThrow
import ui.Profiles
import ui.Profiles.Companion.Profile
class ProfilesManager(): Screen { class ProfilesManager(): Screen {
@Composable @Composable
override fun Content() { override fun Content() {
Profiles.profileList() 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
)
}
}
}
} }
} }

View File

@ -45,22 +45,6 @@ class Settings: Screen {
companion object { companion object {
val userSettings = UserSettings() 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 @Composable
fun setting(setting: Map.Entry<Any?, Any?>) { fun setting(setting: Map.Entry<Any?, Any?>) {
when(setting.key) { when(setting.key) {

View File

@ -1,12 +1,11 @@
<configuration> <configuration>
<logger name="capture_logger" level="DEBUG"> <logger name="capture_logger" level="DEBUG">
<appender-ref ref="CAPTURE_LOGS_FILE" /> <appender-ref ref="CAPTURE_LOGS_FILE" />
<appender-ref ref="STDOUT" />
</logger> </logger>
<root level="INFO"> <root level="INFO">
<appender-ref ref="APPLICATION_LOGS_FILE" />
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
<appender-ref ref="APPLICATION_LOGS_FILE" />
</root> </root>
<!-- Capture appender --> <!-- Capture appender -->