profile screens. ui bugs. styling.

This commit is contained in:
Joshua Perry 2024-10-28 00:46:18 +00:00
parent 66f696567c
commit 10f17f07ab
13 changed files with 462 additions and 165 deletions

1
.gitignore vendored
View File

@ -50,4 +50,5 @@ bin/
### User Generated Files ### ### User Generated Files ###
/logs/ /logs/
/profiles/
/user.settings /user.settings

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.0" /> <option name="version" value="2.0.21" />
</component> </component>
</project> </project>

View File

@ -1,7 +1,9 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("jvm") val kotlinVersion = "2.0.21"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jetbrains.compose") id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
} }

View File

@ -1,12 +1,17 @@
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.windowInsetsPadding 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.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.darkColors import androidx.compose.material.darkColors
import androidx.compose.material.lightColors import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@ -15,6 +20,8 @@ import cafe.adriel.voyager.navigator.Navigator
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import ui.screens.Home import ui.screens.Home
import ui.Bars import ui.Bars
import ui.Profiles.Companion.activeProfile
import ui.Profiles.Companion.getCurrentProfile
import java.io.File import java.io.File
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
@ -23,7 +30,8 @@ val logger = KotlinLogging.logger {}
fun main() = application { fun main() = application {
logger.info { "\n================== NEW APPLICATION SESSION ==================" } logger.info { "\n================== NEW APPLICATION SESSION ==================" }
checkLogsDir() checkDirExists("logs/")
checkDirExists("profiles/")
Window(onCloseRequest = ::exitApplication) { Window(onCloseRequest = ::exitApplication) {
MaterialTheme( MaterialTheme(
colors = if (isSystemInDarkTheme()) darkColors() else lightColors() colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
@ -41,11 +49,30 @@ fun main() = application {
logger.info { "Application started" } logger.info { "Application started" }
} }
fun checkLogsDir() { fun checkDirExists(path: String) {
val dir = File(Paths.get("logs/").absolutePathString()) val dir = File(Paths.get(path).absolutePathString())
if (!dir.exists()) { if (!dir.exists()) {
dir.createNewFile() dir.mkdir()
logger.info { "Folder Created: logs/" } 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
)
} }
} }

View File

@ -28,22 +28,26 @@ class Microphone {
*/ */
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun startCapture(captureDuration: Int) { fun startCapture(captureDuration: Int) {
GlobalScope.async {
val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration
val stt = STT()
captureLogger.info { "{NEW CAPTURE SESSION}" } captureLogger.info { "{NEW CAPTURE SESSION}" }
capturing = true capturing = true
source.open(audioFormat, source.bufferSize) source.open(audioFormat, source.bufferSize)
source.start() source.start()
val buffer = ByteArray(32000 * captureDuration) // sampleRate * sampleSizeInBits * channels * duration
GlobalScope.async {
val stt = STT()
while (capturing) { while (capturing) {
val audioBytes = source.read(buffer, 0, buffer.size) val result = stt.parseBuffer(
val result = stt.parseBuffer(buffer, audioBytes) buffer,
source.read(buffer, 0, buffer.size)
)
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
captureLogger.info { result } captureLogger.info { result }
}
//TODO: Pass onto processing //TODO: Pass onto processing
}
} }
stt.closeModel() stt.closeModel()
} }
@ -64,7 +68,6 @@ class Microphone {
TargetDataLine::class.java, TargetDataLine::class.java,
audioFormat audioFormat
) )
return AudioSystem.getLine(info) as TargetDataLine return AudioSystem.getLine(info) as TargetDataLine
} }
} }

View File

@ -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() { fun closeModel() {
recognizer.close() recognizer.close()

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/all-mpnet-base-v2") return HuggingFaceTokenizer.newInstance("sentence-transformers/paraphrase-mpnet-base-v2")
} }
fun getCriteria(): Criteria<NDList, NDList> = Criteria.builder() fun getCriteria(): Criteria<NDList, NDList> = Criteria.builder()

View File

@ -22,10 +22,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color 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 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 ui.screens.ProfilesManager
import ui.screens.Settings import ui.screens.Settings
class Bars { class Bars {
@ -33,9 +37,9 @@ class Bars {
@Composable @Composable
fun topBar(screenCount: Int) { fun topBar(screenCount: Int) {
ConstraintLayout( ConstraintLayout(
Modifier modifier = Modifier
.fillMaxWidth()
.padding(10.dp) .padding(10.dp)
.fillMaxWidth()
) { ) {
val (back, capture) = createRefs() val (back, capture) = createRefs()
@ -46,51 +50,52 @@ class Bars {
}) })
} }
Capture.captureContext(Modifier.constrainAs(capture) { Capture.captureContext(Modifier.constrainAs(capture) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
linkTo(parent.start, parent.end) linkTo(parent.start, parent.end)
centerVerticallyTo(parent) centerVerticallyTo(parent)
}) })
} }
} }
@Composable @Composable
fun bottomBar() { fun bottomBar() {
ConstraintLayout( ConstraintLayout(
Modifier modifier = Modifier
.padding(5.dp) .padding(5.dp)
.fillMaxWidth() .fillMaxWidth()
) { ) {
val currentProfile = createRef() val (currentProfile, createProfile) = createRefs()
Profiles.currentProfile(Modifier.constrainAs(currentProfile) { Profiles.currentProfile(Modifier.constrainAs(currentProfile) {
bottom.linkTo(parent.bottom) linkTo(parent.top, parent.bottom)
start.linkTo(parent.start) 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 @Composable
fun backButton(modifier: Modifier) { fun backButton(modifier: Modifier) {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
Box( Box(
modifier = modifier modifier = modifier
.background(MaterialTheme.colors.primary, CircleShape) .background(MaterialTheme.colors.primary, CircleShape)
) { ) {
Button( transparentButton(
shape = CircleShape, modifier = Modifier,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.onPrimary), icon = Icons.Filled.ArrowBackIosNew,
elevation = ButtonDefaults.elevation(0.dp), contentDescription = "Go Back",
onClick = { onClick = { navigator.pop() },
navigator.pop()
}) {
Icon(
imageVector = Icons.Filled.ArrowBackIosNew,
contentDescription = "Go Back"
) )
} }
} }
} }
} }
}

View File

@ -1,22 +1,22 @@
package ui package ui
import androidx.compose.foundation.background 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.Column
import androidx.compose.foundation.layout.fillMaxHeight
import processing.Microphone import processing.Microphone
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.shape.RoundedCornerShape 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.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon 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
@ -24,58 +24,61 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color 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
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import logger import logger
import transparentButton
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
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
class Capture { class Capture {
companion object { companion object {
val captureLogger = KotlinLogging.logger("capture_logger") 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 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.startCapture(getCaptureDuration()) else mic.stopCapture()
fun getCaptureIcon(capturing: Boolean) = if (!capturing) Icons.Filled.PlayArrow else Icons.Filled.Stop fun getCaptureIcon(capturing: Boolean) = if (!capturing) Icons.Filled.PlayArrow else Icons.Filled.Stop
fun getCaptureDescription(capturing: Boolean) = if (!capturing) "Start" else "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 @Composable
fun captureContext(modifier: Modifier) { fun captureContext(modifier: Modifier) {
ConstraintLayout( ConstraintLayout(
modifier = modifier modifier = modifier
.background(MaterialTheme.colors.primary, CircleShape) .background(MaterialTheme.colors.primary, CircleShape)
) { ) {
val (button, info, settings) = createRefs() val (button, info, settings) = createRefs()
captureButton(Modifier.constrainAs(button) { captureButton(Modifier.constrainAs(button) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
linkTo(parent.start, info.start, startMargin = 5.dp) linkTo(parent.start, info.start, startMargin = 5.dp)
}) })
lastCapture(Modifier.constrainAs(info) { lastCapture(Modifier.constrainAs(info) {
linkTo(parent.top, parent.bottom) linkTo(parent.top, parent.bottom)
linkTo(button.end, settings.start, startMargin = 5.dp) linkTo(button.end, settings.start, startMargin = 5.dp)
}) })
Settings.settingsButton(Modifier.constrainAs(settings) { 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)
@ -88,115 +91,113 @@ class Capture {
fun captureButton(modifier: Modifier) { fun captureButton(modifier: Modifier) {
val mic = Microphone() val mic = Microphone()
var isCapturing by rememberSaveable { mutableStateOf(mic.capturing) } var isCapturing by rememberSaveable { mutableStateOf(mic.capturing) }
Button(
shape = CircleShape, transparentButton(
colors = ButtonDefaults.buttonColors(contentColor = MaterialTheme.colors.onPrimary, backgroundColor = Color.Transparent),
elevation = ButtonDefaults.elevation(0.dp),
modifier = modifier, modifier = modifier,
icon = getCaptureIcon(isCapturing),
contentDescription = getCaptureDescription(isCapturing) + " audio capture",
onClick = { onClick = {
getCaptureOnClick(mic) getCaptureOnClick(mic)
isCapturing = !isCapturing isCapturing = !isCapturing
} },
) {
Icon(
imageVector = getCaptureIcon(isCapturing),
contentDescription = getCaptureDescription(isCapturing) + " audio capture"
) )
} }
}
@Composable @Composable
fun captureDurationSlider() { fun captureDurationSlider() {
var sliderPosition by remember { mutableStateOf(getCaptureDuration().toFloat()) } var sliderPosition by remember { mutableStateOf(getCaptureDuration().toFloat()) }
Text("Capture Duration") val textModifier = Modifier.padding(vertical = 5.dp)
Text(
modifier = textModifier,
text = "Capture Duration",
)
Slider( Slider(
valueRange = 1.0f..20.0f, valueRange = 1.0f..20.0f,
value = sliderPosition, value = sliderPosition,
onValueChange = { onValueChange = { newValue ->
if (sliderPosition != it) { val newValue = newValue.roundToInt()
sliderPosition = it.roundToInt().toFloat() val newValueFloat = newValue.toFloat()
userSettings.setProperty("capture_duration", "${it.roundToInt()}")
if (sliderPosition != newValueFloat) {
sliderPosition = newValueFloat
userSettings.setProperty("capture_duration", "$newValue")
userSettings.save() 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 @Composable
fun captureLog() { fun captureLog() {
var lines by rememberSaveable { mutableStateOf(getCaptureLog())} var lines: MutableState<MutableList<String>> = rememberSaveable { mutableStateOf(getCaptureLog())}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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()) .verticalScroll(rememberScrollState())
) { ) {
var count = 1 var count = 0
Text( lines.value.add(0, "Capture Log:")
color = MaterialTheme.colors.onBackground,
text = "Capture Log:", SelectionContainer {
modifier = Modifier lines.value.forEach { line ->
.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 val backgroundColor = if (count % 2 == 0) MaterialTheme.colors.onBackground.copy(alpha = 0.05f) else MaterialTheme.colors.background
Text( Text(
color = MaterialTheme.colors.onBackground,
text = line,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.background(backgroundColor, CircleShape) .background(backgroundColor, CircleShape)
.padding(start = 5.dp) .padding(start = 5.dp)
.fillMaxWidth() .fillMaxWidth(),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = line,
) )
count += 1 count++
} }
} }
}
logUpdateCheck(lines)
LaunchedEffect(Unit) {
while(true) {
val newLines = getCaptureLog()
if (newLines != lines) {
lines = newLines
}
delay(getCaptureDuration().toLong() * 1100)
}
}
} }
@Composable @Composable
fun lastCapture(modifier: Modifier) { //TODO: Constrain size so buttons don't move everywhere fun lastCapture(modifier: Modifier) {
var lines by rememberSaveable { mutableStateOf(getCaptureLog()) } val navigator = LocalNavigator.currentOrThrow
var lines: MutableState<MutableList<String>> = rememberSaveable { mutableStateOf(getCaptureLog()) }
Text( Text(
color = MaterialTheme.colors.onPrimary,
modifier = modifier modifier = modifier
.width(300.dp), .width(300.dp)
text = if (lines.isNotEmpty()) lines.last() else "No captures this session", .clickable { navigator.popUntilRoot() },
color = MaterialTheme.colors.onPrimary,
text = lines.value.lastOrNull()?: "No captures this session",
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1 maxLines = 1
) )
logUpdateCheck(lines)
}
@Composable
fun logUpdateCheck(lines: MutableState<MutableList<String>>) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
while(true) { while(true) {
val newLines = getCaptureLog() lines.value = getCaptureLog()
delay(logUpdateDelay)
if (newLines != lines) {
lines = newLines
}
delay((getCaptureDuration().toLong() + 1) * 1000)
} }
} }
} }
} }
} }

View File

@ -4,31 +4,55 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape 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.MaterialTheme
import androidx.compose.material.Text 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.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.SerializationException
import kotlinx.serialization.json.Json 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 ui.Capture.Companion.getCaptureDuration import ui.Capture.Companion.getCaptureDuration
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.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -36,58 +60,230 @@ import kotlin.io.path.absolutePathString
class Profiles { class Profiles {
companion object { 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<Profile> {
val files = File(Paths.get("profiles/").absolutePathString()).listFiles()
files.ifEmpty {
Settings.userSettings.remove("current_profile")
}
return files.map { file ->
load(file)
} as MutableList<Profile>
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun currentProfile(modifier: Modifier) { //TODO: store profile in mutableState so component updates when currentProfile fun profileList() {
var profile by remember { mutableStateOf(getCurrentProfile()) } var showActivateDialog by rememberSaveable { mutableStateOf(false) }
var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
var selectedProfile by rememberSaveable { mutableStateOf<Profile>(Profile()) }
val navigator = LocalNavigator.currentOrThrow 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( Text(
color = MaterialTheme.colors.onPrimary,
text = if (profile != null) profile!!.name else "Not Selected",
textAlign = TextAlign.Center,
modifier = modifier modifier = modifier
.background(MaterialTheme.colors.primary, CircleShape) .background(MaterialTheme.colors.primary, CircleShape)
.padding(5.dp) .padding(5.dp)
.width(150.dp) .width(150.dp)
.combinedClickable( .combinedClickable(
onClick = { if (profile != null) navigator.push(ProfileDetails(profile!!)) }, onClick = { navigator.push(ProfileDetails(activeProfile.value)) },
onDoubleClick = { navigator.push(ProfilesManager()) } onDoubleClick = {
) if (navigator.lastItem !is ProfilesManager) {
navigator.push(ProfilesManager())
}
}
),
color = MaterialTheme.colors.onPrimary,
textAlign = TextAlign.Center,
text = activeProfile.value.name
) )
} }
@Serializable
data class Profile( data class Profile(
val name: String, val name: String = "{NO NAME}",
@SerialName("ai_name") @SerialName("ai_name")
val aiName: String, val aiName: String = "{NO NAME}",
@SerialName("program_name") @SerialName("program_name")
val programName: Path, val programName: String = "{NO NAME}",
val commands: List<Command> @SerialName("program_path")
val programPath: String = "{NO NAME}",
@SerialName("commands")
val commands: List<Command>? = null,
) )
@Serializable
data class Command( data class Command(
@SerialName("wording_Variants") @SerialName("wording_Variants")
val wordingVariants: List<String>, val wordingVariants: List<String>? = null,
val triggers: List<String> val triggers: List<String>? = null
) )
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun load(profile: String?): Profile? { fun load(profile: File?): Profile {
if (profile != null) { var file: Profile
val stream = FileInputStream(getProfileLocation(profile))
val file = Json.decodeFromStream<Profile>(stream) try {
val stream = FileInputStream(profile!!)
file = Json.decodeFromStream<Profile>(stream).copy(name = profile.nameWithoutExtension)
stream.close() stream.close()
}
catch (ne: FileNotFoundException) {
file = Profile(name = "Not Selected")
}
catch (je: SerializationException) {
file = Profile(name = profile!!.nameWithoutExtension)
}
return file return file
}
else {
return null
}
//TODO: Add logging messages //TODO: Add logging messages
} }
fun load(profile: String?): Profile = load(File(getProfileLocation(profile)))
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun save(profile: Profile) { fun save(profile: Profile) {
val stream = FileOutputStream(getProfileLocation(profile.name)) val stream = FileOutputStream(getProfileLocation(profile.name))
@ -96,6 +292,5 @@ class Profiles {
stream.close() stream.close()
} }
fun getProfileLocation(profile: String) = Paths.get("profiles/$profile.profile").absolutePathString()
} }
} }

View File

@ -4,7 +4,7 @@ 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 ui.Profiles.Companion.Profile
class ProfileDetails(profile: Profile): Screen { class ProfileDetails(profile: Profile?): Screen {
@Composable @Composable
override fun Content() { override fun Content() {
//TODO: Display all fields for profile //TODO: Display all fields for profile

View File

@ -1,14 +1,70 @@
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 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() {
//TODO: List of profiles in profiles folder onDoubleClick = ProfileDetails for that profile, longClick = open confirmation dialog to activate profile. Show name, programName, and aiName Profiles.profileList()
// Create Profile button }
// Delete Profile button
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

@ -1,12 +1,12 @@
package ui.screens package ui.screens
import androidx.compose.foundation.background
import ui.UserSettings import ui.UserSettings
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer 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.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.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons 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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.CurrentScreen
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 ui.Capture import ui.Capture
class Settings: Screen { class Settings: Screen {
@Composable @Composable
override fun Content() { override fun Content() {
Column { Column(
modifier = Modifier
.padding(10.dp)
) {
userSettings.forEach { setting -> userSettings.forEach { setting ->
Spacer(Modifier.padding(10.dp))
setting(setting) setting(setting)
Spacer(
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.onBackground)
)
} }
} }
} }
@ -37,21 +48,18 @@ class Settings: Screen {
@Composable @Composable
fun settingsButton(modifier: Modifier) { fun settingsButton(modifier: Modifier) {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
Button(
shape = CircleShape, transparentButton(
colors = ButtonDefaults.buttonColors(contentColor = MaterialTheme.colors.onPrimary ,backgroundColor = Color.Transparent),
elevation = ButtonDefaults.elevation(0.dp),
modifier = modifier, modifier = modifier,
icon = Icons.Filled.Settings,
contentDescription = "Settings",
onClick = { onClick = {
if (navigator.lastItem !is Settings) {
navigator.push(Settings()) navigator.push(Settings())
} }
) { },
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
) )
} }
}
@Composable @Composable
fun setting(setting: Map.Entry<Any?, Any?>) { fun setting(setting: Map.Entry<Any?, Any?>) {
@ -61,6 +69,5 @@ class Settings: Screen {
else -> null else -> null
} }
} }
} }
} }