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