From 10f17f07ab17b2530829de4e11ad5c2b83476bb4 Mon Sep 17 00:00:00 2001 From: r0r-5chach Date: Mon, 28 Oct 2024 00:46:18 +0000 Subject: [PATCH] profile screens. ui bugs. styling. --- .gitignore | 1 + .idea/kotlinc.xml | 2 +- build.gradle.kts | 4 +- src/main/kotlin/Main.kt | 43 +++- src/main/kotlin/processing/Microphone.kt | 23 +- src/main/kotlin/processing/STT.kt | 2 +- .../kotlin/processing/SemanticSimilarity.kt | 2 +- src/main/kotlin/ui/Bars.kt | 45 ++-- src/main/kotlin/ui/Capture.kt | 155 +++++------ src/main/kotlin/ui/Profiles.kt | 241 ++++++++++++++++-- src/main/kotlin/ui/screens/ProfileDetails.kt | 2 +- src/main/kotlin/ui/screens/ProfilesManager.kt | 64 ++++- src/main/kotlin/ui/screens/Settings.kt | 43 ++-- 13 files changed, 462 insertions(+), 165 deletions(-) diff --git a/.gitignore b/.gitignore index aeb22ac..41707d0 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,5 @@ bin/ ### User Generated Files ### /logs/ +/profiles/ /user.settings diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 6d0ee1c..c224ad5 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 59d6aa7..49e55c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,9 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { - kotlin("jvm") + val kotlinVersion = "2.0.21" + kotlin("jvm") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion id("org.jetbrains.compose") id("org.jetbrains.kotlin.plugin.compose") } diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 5863d73..c67607b 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,12 +1,17 @@ import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.darkColors import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @@ -15,6 +20,8 @@ import cafe.adriel.voyager.navigator.Navigator import io.github.oshai.kotlinlogging.KotlinLogging import ui.screens.Home import ui.Bars +import ui.Profiles.Companion.activeProfile +import ui.Profiles.Companion.getCurrentProfile import java.io.File import java.nio.file.Paths import kotlin.io.path.absolutePathString @@ -23,7 +30,8 @@ val logger = KotlinLogging.logger {} fun main() = application { logger.info { "\n================== NEW APPLICATION SESSION ==================" } - checkLogsDir() + checkDirExists("logs/") + checkDirExists("profiles/") Window(onCloseRequest = ::exitApplication) { MaterialTheme( colors = if (isSystemInDarkTheme()) darkColors() else lightColors() @@ -41,14 +49,33 @@ fun main() = application { logger.info { "Application started" } } -fun checkLogsDir() { - val dir = File(Paths.get("logs/").absolutePathString()) +fun checkDirExists(path: String) { + val dir = File(Paths.get(path).absolutePathString()) if (!dir.exists()) { - dir.createNewFile() - logger.info { "Folder Created: logs/" } + dir.mkdir() + logger.info { "Folder Created: $path" } } } +@Composable +fun transparentButton(icon: ImageVector, contentDescription: String, onClick: () -> Unit, modifier: Modifier) { + Button( + onClick = onClick, + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colors.onPrimary, + backgroundColor = Color.Transparent + ), + elevation = ButtonDefaults.elevation(0.dp), + modifier = modifier + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription + ) + } +} + //TODO: If folders (Semantic-model/, STT-model/) don't exist on start-up install // Install Semantic-model by downloading, using djl-converter (look into using pip module from Java), move to resources // Install STT-model by downloading, move to resources diff --git a/src/main/kotlin/processing/Microphone.kt b/src/main/kotlin/processing/Microphone.kt index fb73ff5..d156bb2 100644 --- a/src/main/kotlin/processing/Microphone.kt +++ b/src/main/kotlin/processing/Microphone.kt @@ -28,22 +28,26 @@ class Microphone { */ @OptIn(DelicateCoroutinesApi::class) fun startCapture(captureDuration: Int) { - captureLogger.info { "{NEW CAPTURE SESSION}" } - capturing = true - source.open(audioFormat, source.bufferSize) - source.start() - - val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration GlobalScope.async { + 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() + while (capturing) { - val audioBytes = source.read(buffer, 0, buffer.size) - val result = stt.parseBuffer(buffer, audioBytes) + val result = stt.parseBuffer( + buffer, + source.read(buffer, 0, buffer.size) + ) + if (result.isNotEmpty()) { captureLogger.info { result } + //TODO: Pass onto processing + } - //TODO: Pass onto processing } stt.closeModel() } @@ -64,7 +68,6 @@ class Microphone { TargetDataLine::class.java, audioFormat ) - return AudioSystem.getLine(info) as TargetDataLine } } \ No newline at end of file diff --git a/src/main/kotlin/processing/STT.kt b/src/main/kotlin/processing/STT.kt index b0144da..88cbcc2 100644 --- a/src/main/kotlin/processing/STT.kt +++ b/src/main/kotlin/processing/STT.kt @@ -15,7 +15,7 @@ class STT { /** * */ - fun parseBuffer(buffer: ByteArray, audioBytes: Int): String = if (recognizer.acceptWaveForm(buffer, audioBytes)) recognizer.result.split("\"")[3] else recognizer.partialResult.split("\"")[3] + fun parseBuffer(buffer: ByteArray, audioBytes: Int): String = if (recognizer.acceptWaveForm(buffer, audioBytes)) recognizer.finalResult.split("\"")[3] else recognizer.result.split("\"")[3] fun closeModel() { recognizer.close() diff --git a/src/main/kotlin/processing/SemanticSimilarity.kt b/src/main/kotlin/processing/SemanticSimilarity.kt index 9df086c..912b9e9 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/all-mpnet-base-v2") + return HuggingFaceTokenizer.newInstance("sentence-transformers/paraphrase-mpnet-base-v2") } fun getCriteria(): Criteria = Criteria.builder() diff --git a/src/main/kotlin/ui/Bars.kt b/src/main/kotlin/ui/Bars.kt index c4e3dbd..a1457b3 100644 --- a/src/main/kotlin/ui/Bars.kt +++ b/src/main/kotlin/ui/Bars.kt @@ -22,10 +22,14 @@ 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.screens.ProfilesManager import ui.screens.Settings class Bars { @@ -33,9 +37,9 @@ class Bars { @Composable fun topBar(screenCount: Int) { ConstraintLayout( - Modifier - .fillMaxWidth() + modifier = Modifier .padding(10.dp) + .fillMaxWidth() ) { val (back, capture) = createRefs() @@ -46,50 +50,51 @@ class Bars { }) } + Capture.captureContext(Modifier.constrainAs(capture) { linkTo(parent.top, parent.bottom) linkTo(parent.start, parent.end) centerVerticallyTo(parent) }) - } } @Composable fun bottomBar() { ConstraintLayout( - Modifier + modifier = Modifier .padding(5.dp) .fillMaxWidth() ) { - val currentProfile = createRef() + val (currentProfile, createProfile) = createRefs() Profiles.currentProfile(Modifier.constrainAs(currentProfile) { - bottom.linkTo(parent.bottom) + linkTo(parent.top, parent.bottom) start.linkTo(parent.start) }) - } - } + if (LocalNavigator.currentOrThrow.lastItem is ProfilesManager) { + Profiles.createProfile(Modifier.constrainAs(createProfile) { + linkTo(parent.top, parent.bottom) + end.linkTo(parent.end) + }) + } + } + }p @Composable fun backButton(modifier: Modifier) { val navigator = LocalNavigator.currentOrThrow + Box( modifier = modifier .background(MaterialTheme.colors.primary, CircleShape) ) { - Button( - shape = CircleShape, - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.onPrimary), - elevation = ButtonDefaults.elevation(0.dp), - onClick = { - navigator.pop() - }) { - Icon( - imageVector = Icons.Filled.ArrowBackIosNew, - contentDescription = "Go Back" - ) - } + transparentButton( + modifier = Modifier, + icon = Icons.Filled.ArrowBackIosNew, + contentDescription = "Go Back", + onClick = { navigator.pop() }, + ) } } } diff --git a/src/main/kotlin/ui/Capture.kt b/src/main/kotlin/ui/Capture.kt index ba7f950..3ea14a0 100644 --- a/src/main/kotlin/ui/Capture.kt +++ b/src/main/kotlin/ui/Capture.kt @@ -1,22 +1,22 @@ package ui import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight import processing.Microphone 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.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape +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 @@ -24,58 +24,61 @@ import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop 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.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow 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 import androidx.constraintlayout.compose.ConstraintLayout +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.delay import logger +import transparentButton import ui.screens.Settings import ui.screens.Settings.Companion.userSettings import java.io.File import java.nio.file.Paths import kotlin.io.path.absolutePathString -import kotlin.math.max import kotlin.math.roundToInt class Capture { companion object { 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 getCaptureLog() = File(Paths.get("logs/capture.log").absolutePathString()).readLines() + fun getCaptureLog() = File(Paths.get("logs/capture.log").absolutePathString()).readLines().toMutableList() @Composable fun captureContext(modifier: Modifier) { ConstraintLayout( modifier = modifier .background(MaterialTheme.colors.primary, CircleShape) - ) { val (button, info, settings) = createRefs() + captureButton(Modifier.constrainAs(button) { linkTo(parent.top, parent.bottom) linkTo(parent.start, info.start, startMargin = 5.dp) }) + lastCapture(Modifier.constrainAs(info) { linkTo(parent.top, parent.bottom) linkTo(button.end, settings.start, startMargin = 5.dp) }) + Settings.settingsButton(Modifier.constrainAs(settings) { linkTo(parent.top, parent.bottom) linkTo(info.end, parent.end, startMargin = 5.dp, endMargin = 5.dp) @@ -88,115 +91,113 @@ class Capture { fun captureButton(modifier: Modifier) { val mic = Microphone() var isCapturing by rememberSaveable { mutableStateOf(mic.capturing) } - Button( - shape = CircleShape, - colors = ButtonDefaults.buttonColors(contentColor = MaterialTheme.colors.onPrimary, backgroundColor = Color.Transparent), - elevation = ButtonDefaults.elevation(0.dp), + + transparentButton( modifier = modifier, - onClick = { + icon = getCaptureIcon(isCapturing), + contentDescription = getCaptureDescription(isCapturing) + " audio capture", + onClick = { getCaptureOnClick(mic) isCapturing = !isCapturing - } - ) { - Icon( - imageVector = getCaptureIcon(isCapturing), - contentDescription = getCaptureDescription(isCapturing) + " audio capture" - ) - } + }, + ) } @Composable fun captureDurationSlider() { var sliderPosition by remember { mutableStateOf(getCaptureDuration().toFloat()) } - Text("Capture Duration") + val textModifier = Modifier.padding(vertical = 5.dp) + + + Text( + modifier = textModifier, + text = "Capture Duration", + ) + Slider( valueRange = 1.0f..20.0f, value = sliderPosition, - onValueChange = { - if (sliderPosition != it) { - sliderPosition = it.roundToInt().toFloat() - userSettings.setProperty("capture_duration", "${it.roundToInt()}") + onValueChange = { newValue -> + val newValue = newValue.roundToInt() + val newValueFloat = newValue.toFloat() + + if (sliderPosition != newValueFloat) { + sliderPosition = newValueFloat + userSettings.setProperty("capture_duration", "$newValue") userSettings.save() - logger.info { "User Setting Changed: capture_duration = ${it.roundToInt()}" } + logger.info { "User Setting Changed: capture_duration = $newValue" } } - } + }, + ) + + Text( + modifier = textModifier, + text = "$sliderPosition seconds", ) - Text("$sliderPosition seconds") } @Composable fun captureLog() { - var lines by rememberSaveable { mutableStateOf(getCaptureLog())} + var lines: MutableState> = rememberSaveable { mutableStateOf(getCaptureLog())} Column( modifier = Modifier .fillMaxWidth() - .padding(start = 25.dp, end = 25.dp, top = 10.dp, bottom = 50.dp) + .padding(horizontal = 25.dp) + .padding(top = 10.dp, bottom = 50.dp) .verticalScroll(rememberScrollState()) ) { - var count = 1 - Text( - color = MaterialTheme.colors.onBackground, - text = "Capture Log:", - modifier = Modifier - .background(MaterialTheme.colors.onBackground.copy(alpha = 0.05f), CircleShape) - .padding(start = 5.dp) - .fillMaxWidth() - ) - lines.forEach { line -> - val backgroundColor = if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background - Text( - color = MaterialTheme.colors.onBackground, - text = line, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .background(backgroundColor, CircleShape) - .padding(start = 5.dp) - .fillMaxWidth() - ) - count += 1 - } - } + var count = 0 + lines.value.add(0, "Capture Log:") - LaunchedEffect(Unit) { - while(true) { - val newLines = getCaptureLog() - - if (newLines != lines) { - lines = newLines + SelectionContainer { + lines.value.forEach { line -> + val backgroundColor = if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background + Text( + modifier = Modifier + .background(backgroundColor, CircleShape) + .padding(start = 5.dp) + .fillMaxWidth(), + color = MaterialTheme.colors.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = line, + ) + count++ } - - delay(getCaptureDuration().toLong() * 1100) } } + logUpdateCheck(lines) + } @Composable - fun lastCapture(modifier: Modifier) { //TODO: Constrain size so buttons don't move everywhere - var lines by rememberSaveable { mutableStateOf(getCaptureLog()) } + fun lastCapture(modifier: Modifier) { + val navigator = LocalNavigator.currentOrThrow + var lines: MutableState> = rememberSaveable { mutableStateOf(getCaptureLog()) } Text( - color = MaterialTheme.colors.onPrimary, modifier = modifier - .width(300.dp), - text = if (lines.isNotEmpty()) lines.last() else "No captures this session", + .width(300.dp) + .clickable { navigator.popUntilRoot() }, + color = MaterialTheme.colors.onPrimary, + text = lines.value.lastOrNull()?: "No captures this session", overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, maxLines = 1 ) + + logUpdateCheck(lines) + } + + @Composable + fun logUpdateCheck(lines: MutableState>) { LaunchedEffect(Unit) { while(true) { - val newLines = getCaptureLog() - - if (newLines != lines) { - lines = newLines - } - - delay((getCaptureDuration().toLong() + 1) * 1000) + lines.value = getCaptureLog() + delay(logUpdateDelay) } } } - } } \ No newline at end of file diff --git a/src/main/kotlin/ui/Profiles.kt b/src/main/kotlin/ui/Profiles.kt index 1c232d2..55302b5 100644 --- a/src/main/kotlin/ui/Profiles.kt +++ b/src/main/kotlin/ui/Profiles.kt @@ -4,31 +4,55 @@ 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.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +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.Cancel +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Create 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 +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream +import transparentButton import ui.Capture.Companion.getCaptureDuration 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 @@ -36,58 +60,230 @@ import kotlin.io.path.absolutePathString class Profiles { companion object { - fun getCurrentProfile() = load(Settings.userSettings.getProperty("current_profile", null)) + val activeProfile = mutableStateOf(getCurrentProfile()) + val profiles = mutableStateOf(getProfiles()) + + fun getCurrentProfile() = load(Settings.userSettings.getProperty("current_profile")) + fun getProfileLocation(profile: String?) = Paths.get("profiles/$profile.profile").absolutePathString() + fun getProfiles(): MutableList { + val files = File(Paths.get("profiles/").absolutePathString()).listFiles() + + files.ifEmpty { + Settings.userSettings.remove("current_profile") + } + + return files.map { file -> + load(file) + } as MutableList + } @OptIn(ExperimentalFoundationApi::class) @Composable - fun currentProfile(modifier: Modifier) { //TODO: store profile in mutableState so component updates when currentProfile - var profile by remember { mutableStateOf(getCurrentProfile()) } + 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( - color = MaterialTheme.colors.onPrimary, - text = if (profile != null) profile!!.name else "Not Selected", - textAlign = TextAlign.Center, modifier = modifier .background(MaterialTheme.colors.primary, CircleShape) .padding(5.dp) .width(150.dp) .combinedClickable( - onClick = { if (profile != null) navigator.push(ProfileDetails(profile!!)) }, - onDoubleClick = { navigator.push(ProfilesManager()) } - ) + 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, + val name: String = "{NO NAME}", @SerialName("ai_name") - val aiName: String, + val aiName: String = "{NO NAME}", @SerialName("program_name") - val programName: Path, - val commands: List + 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, - val triggers: List + val wordingVariants: List? = null, + val triggers: List? = null ) @OptIn(ExperimentalSerializationApi::class) - fun load(profile: String?): Profile? { - if (profile != null) { - val stream = FileInputStream(getProfileLocation(profile)) - val file = Json.decodeFromStream(stream) + fun load(profile: File?): Profile { + var file: Profile + + try { + val stream = FileInputStream(profile!!) + file = Json.decodeFromStream(stream).copy(name = profile.nameWithoutExtension) stream.close() - return file } - else { - return null + catch (ne: FileNotFoundException) { + file = Profile(name = "Not Selected") } + catch (je: SerializationException) { + file = Profile(name = profile!!.nameWithoutExtension) + } + + return file //TODO: Add logging messages } + fun load(profile: String?): Profile = load(File(getProfileLocation(profile))) + @OptIn(ExperimentalSerializationApi::class) fun save(profile: Profile) { val stream = FileOutputStream(getProfileLocation(profile.name)) @@ -96,6 +292,5 @@ class Profiles { stream.close() } - fun getProfileLocation(profile: String) = Paths.get("profiles/$profile.profile").absolutePathString() } } \ 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 ce952dc..a8ed858 100644 --- a/src/main/kotlin/ui/screens/ProfileDetails.kt +++ b/src/main/kotlin/ui/screens/ProfileDetails.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import cafe.adriel.voyager.core.screen.Screen import ui.Profiles.Companion.Profile -class ProfileDetails(profile: Profile): Screen { +class ProfileDetails(profile: Profile?): Screen { @Composable override fun Content() { //TODO: Display all fields for profile diff --git a/src/main/kotlin/ui/screens/ProfilesManager.kt b/src/main/kotlin/ui/screens/ProfilesManager.kt index 165a828..6450fa4 100644 --- a/src/main/kotlin/ui/screens/ProfilesManager.kt +++ b/src/main/kotlin/ui/screens/ProfilesManager.kt @@ -1,14 +1,70 @@ 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 -class ProfilesManager: Screen { +class ProfilesManager(): Screen { @Composable override fun Content() { - //TODO: List of profiles in profiles folder onDoubleClick = ProfileDetails for that profile, longClick = open confirmation dialog to activate profile. Show name, programName, and aiName - // Create Profile button - // Delete Profile button + 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 + ) + } + } + + + } } } \ 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 3fcc088..175898b 100644 --- a/src/main/kotlin/ui/screens/Settings.kt +++ b/src/main/kotlin/ui/screens/Settings.kt @@ -1,12 +1,12 @@ package ui.screens +import androidx.compose.foundation.background import ui.UserSettings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -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 @@ -16,17 +16,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import transparentButton import ui.Capture class Settings: Screen { @Composable override fun Content() { - Column { + Column( + modifier = Modifier + .padding(10.dp) + ) { userSettings.forEach { setting -> - Spacer(Modifier.padding(10.dp)) setting(setting) + + Spacer( + modifier = Modifier + .height(1.dp) + .fillMaxWidth() + .background(MaterialTheme.colors.onBackground) + ) } } } @@ -37,20 +48,17 @@ class Settings: Screen { @Composable fun settingsButton(modifier: Modifier) { val navigator = LocalNavigator.currentOrThrow - Button( - shape = CircleShape, - colors = ButtonDefaults.buttonColors(contentColor = MaterialTheme.colors.onPrimary ,backgroundColor = Color.Transparent), - elevation = ButtonDefaults.elevation(0.dp), + + transparentButton( modifier = modifier, + icon = Icons.Filled.Settings, + contentDescription = "Settings", onClick = { - navigator.push(Settings()) - } - ) { - Icon( - Icons.Filled.Settings, - contentDescription = "Settings" - ) - } + if (navigator.lastItem !is Settings) { + navigator.push(Settings()) + } + }, + ) } @Composable @@ -61,6 +69,5 @@ class Settings: Screen { else -> null } } - } } \ No newline at end of file