Compare commits

..

5 Commits

Author SHA1 Message Date
1f9ff73a6d Add git ignore 2025-02-09 18:42:18 +01:00
56e7c53f70 Add migrations 2025-02-09 18:33:04 +01:00
66617923b7 Add multiple languages 2025-02-09 18:31:51 +01:00
47e1b47028 Fix bugs and add direction of translation 2025-02-02 17:00:26 +01:00
52eef8dbd4 Cosmetics 2025-02-02 16:23:58 +01:00
15 changed files with 334 additions and 110 deletions

4
.gitignore vendored
View File

@@ -11,8 +11,8 @@ captures
.externalNativeBuild
.cxx
*.xcodeproj/*
data/**
composeApp/data/**
data/*
composeApp/data/*
composeApp/src/commonMain/resources/media/**
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/

View File

@@ -69,6 +69,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
@@ -86,8 +87,7 @@ kotlin {
implementation(libs.composeIcons.tablerIcons)
implementation(libs.composeIcons.fontAwesome)
implementation(libs.korau)
implementation(libs.flagkit)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)

View File

@@ -21,6 +21,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.observeOn
import org.koin.compose.koinInject
@@ -34,30 +36,78 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import compose.icons.TablerIcons
import compose.icons.tablericons.Table
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.itemsIndexed
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
import compose.icons.tablericons.ArrowBigRight
import compose.icons.tablericons.FileText
import compose.icons.fontawesomeicons.solid.PlayCircle
import compose.icons.tablericons.ArrowBigLeft
import models.Term
import models.TermFull
import kotlinx.coroutines.*
import service.DatabaseFactory.getTranslationForLanguages
import service.DatabaseFactory.getLanguageForCode
import service.DatabaseFactory.getDictionaies
import service.DatabaseFactory.getLanguageForId
import dev.carlsen.flagkit.FlagKit
import dev.carlsen.flagkit.FlagIcons
import models.Dictionary
@Composable
fun RowScope.TableCell(
text: String,
weight: Float
weight: Float,
color: Color = Color.Gray
) {
Text(
text = text,
Modifier
.border(1.dp, Color.Black)
.border(1.dp, Color.LightGray)
.weight(weight)
.padding(4.dp)
)
}
@Composable
fun LangugeDropdown(onChangeClick: (dict: Dictionary) -> Unit,dictionary: Dictionary?) {
var expanded by remember { mutableStateOf(false) }
fun groupByString1(terms: List<TermFull>): MutableList<MutableList<TermFull>> {
IconButton(onClick = {
expanded = !expanded
}) {
getLanguageForId(dictionary!!.lang1Id)?.let{ l ->
FlagKit.getFlag(countryCode = l.alphaCode!!)?.let {
Image(
imageVector = it,
contentDescription = "English",
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
for ((id, dict) in getDictionaies()) {
getLanguageForId(dict.lang1Id)?.let { l ->
DropdownMenuItem(
leadingIcon = {
Image(
imageVector = FlagKit.getFlag(countryCode = l.alphaCode!!)!!,
contentDescription = l.name
)
},
text = { Text(l.name) },
onClick = { onChangeClick(dict); expanded = false }
)
}
}
}
}
}
fun groupByString(terms: List<TermFull>, direction: Int): MutableList<MutableList<TermFull>> {
val fullList: MutableList<MutableList<TermFull>> = mutableListOf()
var lastTerm: TermFull? = null
var termsEq: MutableList<TermFull> = mutableListOf()
@@ -65,7 +115,9 @@ fun groupByString1(terms: List<TermFull>): MutableList<MutableList<TermFull>> {
if (lastTerm == null) {
termsEq += t
} else {
if (lastTerm.string1 == t.string1){
val s1 = if (direction == 1) lastTerm.string1 else lastTerm.string2
val s2 = if (direction == 1) t.string1 else t.string2
if (s1 == s2) {
termsEq += t
} else {
fullList += termsEq
@@ -83,8 +135,11 @@ fun groupByString1(terms: List<TermFull>): MutableList<MutableList<TermFull>> {
@Composable
fun SearchBarTextField(viewModel: MainModelView) {
val query = remember { mutableStateOf("") }
val columnViewType = remember { mutableStateOf(false) }
var columnViewType = remember { mutableStateOf(false) }
var tDirection = remember { mutableStateOf(1) }
Column {
val dict by viewModel.dictionary.collectAsState()
Row(
verticalAlignment = Alignment.CenterVertically,
// horizontalArrangement = Arrangement.End,
@@ -95,7 +150,14 @@ fun SearchBarTextField(viewModel: MainModelView) {
value = query.value,
onValueChange = {
query.value = it
viewModel.getTerms(query.value)
if (dict == null) return@TextField
val l1 = getLanguageForId(dict!!.lang1Id)
val l2 = getLanguageForId(dict!!.lang2Id)
if (l1 != null && l2 != null) {
val trans = if (tDirection.value == 1) getTranslationForLanguages(l1.shortName,l2.shortName) else getTranslationForLanguages(l2.shortName,l1.shortName)
if (trans == null) return@TextField
viewModel.getTerms(query.value, trans)
}
},
placeholder = { Text("Prelož...") },
singleLine = true,
@@ -112,18 +174,56 @@ fun SearchBarTextField(viewModel: MainModelView) {
},
modifier = Modifier.weight(3f)
)
Row (Modifier.width(155.dp).border(width = 2.dp, Color.Black)) {
Row(Modifier.width(200.dp).border(width = 2.dp, Color.Black)) {
Spacer(modifier = Modifier.padding(2.dp))
Icon(TablerIcons.Table, contentDescription = "Table View", modifier = Modifier.size(width = 50.dp, height = 50.dp))
IconButton(onClick = {
columnViewType.value = !columnViewType.value
}) {
if (columnViewType.value)
Icon(
TablerIcons.Table,
contentDescription = "Table View",
modifier = Modifier.size(width = 50.dp, height = 50.dp)
)
else
Icon(
TablerIcons.FileText,
contentDescription = "List View",
modifier = Modifier.size(width = 50.dp, height = 50.dp)
)
}
Spacer(modifier = Modifier.padding(2.dp))
Icon(TablerIcons.ArrowBigRight, contentDescription = "Table View", modifier = Modifier.size(width = 50.dp, height = 50.dp))
IconButton(onClick = {
tDirection.value = if (tDirection.value == 1) 2 else 1
}) {
if (tDirection.value == 1)
Icon(
TablerIcons.ArrowBigRight,
contentDescription = "Forward",
modifier = Modifier.size(width = 50.dp, height = 50.dp)
)
else
Icon(
TablerIcons.ArrowBigLeft,
contentDescription = "Resverse",
modifier = Modifier.size(width = 50.dp, height = 50.dp)
)
}
LangugeDropdown(
onChangeClick = viewModel::changeDictionary,
dictionary = dict
)
Spacer(modifier = Modifier.padding(2.dp))
Icon(Icons.Filled.Settings, contentDescription = "Table View", modifier = Modifier.size(width = 50.dp, height = 50.dp))
Icon(
Icons.Filled.Settings,
contentDescription = "Table View",
modifier = Modifier.size(width = 50.dp, height = 50.dp)
)
//Spacer(modifier = Modifier.fillMaxWidth())
}
}
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
val terms by viewModel.terms.collectAsState()
if (terms != null) {
@@ -131,20 +231,30 @@ fun SearchBarTextField(viewModel: MainModelView) {
Column {
if (columnViewType.value) {
Row(Modifier.background(Color.Gray)) {
TableCell(text = "Anglicky", weight = .5f)
TableCell(text = "Slovensky", weight = .5f)
TableCell(text = if (tDirection.value == 1) "Anglicky" else "Slovensky", weight = .5f)
TableCell(text = if (tDirection.value == 1) "Slovensky" else "Anglicky", weight = .5f)
}
val column1Weight = .5f // 50%
val column2Weight = .5f // 50%
// The LazyColumn will be our table. Notice the use of the weights below
LazyColumn(Modifier.fillMaxSize().padding(0.dp)) {
// Here is the header
itemsIndexed(items = terms!!, itemContent = { i, t ->
val color = if (i % 2 == 1) Color.hsl(
hue = 168f,
saturation = .77f,
lightness = .68f,
alpha = 1f
) else Color.hsl(hue = 217f, saturation = .77f, lightness = .68f, alpha = 1f)
items(items = terms!!, itemContent = { t ->
Row(Modifier.fillMaxWidth()) {
TableCell(text = t.string1, weight = column1Weight)
TableCell(text = t.string2, weight = column2Weight)
Row(Modifier.fillMaxWidth().background(color)) {
TableCell(
text = if (tDirection.value == 1) t.string1 else t.string2,
weight = column1Weight
)
TableCell(
text = if (tDirection.value == 1) t.string2 else t.string1,
weight = column2Weight
)
}
@@ -152,23 +262,25 @@ fun SearchBarTextField(viewModel: MainModelView) {
}
} else {
val terms2: MutableList<MutableList<TermFull>> = groupByString1(terms!!)
val terms2: MutableList<MutableList<TermFull>> = groupByString(terms!!, tDirection.value)
LazyColumn(Modifier.fillMaxSize().padding(0.dp)) {
var i=0
var rowBackground = Color.LightGray
items(items = terms2, itemContent = { t2 ->
var t : TermFull? = null
itemsIndexed(items = terms2, itemContent = { i, t2 ->
var t: TermFull? = null
if (t2.size > 0) t = t2[0]
else return@items
when ( i++ % 2 ) {
else return@itemsIndexed
when (i % 2) {
0 -> rowBackground = Color.LightGray
1 -> rowBackground = Color.Gray
}
Row(Modifier.fillMaxWidth().background(color = rowBackground)) {
Column {
Row( verticalAlignment = Alignment.CenterVertically) {
Text(t.string1, fontWeight = FontWeight.Bold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
if (tDirection.value == 1) t.string1 else t.string2,
fontWeight = FontWeight.Bold
)
for (p in t.pronunciations!!) {
Spacer(modifier = Modifier.padding(2.dp))
if (p.ipa != null) {
@@ -186,21 +298,29 @@ fun SearchBarTextField(viewModel: MainModelView) {
onClick = {
viewModel.playSound("media/${p.filename}")
}) {
Icon(FontAwesomeIcons.Solid.PlayCircle,contentDescription = "Play sound",Modifier.size(20.dp).padding(start = 2.dp),)
Icon(
FontAwesomeIcons.Solid.PlayCircle,
contentDescription = "Play sound",
Modifier.size(20.dp).padding(start = 2.dp),
)
}
}
Spacer(modifier = Modifier.padding(2.dp))
}
}
val str2 = t2.joinToString(separator = ", ") { it.string2 }
val str2 =
if (tDirection.value == 1) t2.joinToString(separator = ", ") { it.string2 } else t2.joinToString(
separator = ", "
) { it.string1 }
Text(text = str2, modifier = Modifier.padding(start = 20.dp))
Spacer(modifier = Modifier
Spacer(
modifier = Modifier
.border(1.dp, Color.Black)
.padding(1.dp).fillMaxWidth())
.padding(1.dp).fillMaxWidth()
)
}
}
})
@@ -216,9 +336,8 @@ fun SearchBarTextField(viewModel: MainModelView) {
}
@Composable
fun MainView(viewModel: MainModelView = koinInject<MainModelView>(),) {
fun MainView(viewModel: MainModelView = koinInject<MainModelView>()) {
SearchBarTextField(viewModel)
}

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import korlibs.audio.sound.*
import korlibs.io.file.std.resourcesVfs
import korlibs.io.file.std.ZipVfs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -13,21 +14,34 @@ import service.DatabaseFactory.getTranslationForLanguages
import service.SearchType
import service.TermServiceImpl
import korlibs.audio.format.*
import korlibs.io.compression.zip.ZipEntry
import korlibs.io.file.VfsFile
import korlibs.io.file.std.openAsZip
import models.Dictionary
import models.Translation
import service.DatabaseFactory.getDictionaies
class MainModelView(private val repository: TermServiceImpl): ViewModel() {
val terms : StateFlow<List<TermFull>?> get() = _terms
private val _terms = MutableStateFlow<List<TermFull>?>(null)
fun getTerms(term: String) {
val dictionary: StateFlow<Dictionary?> get() = _dictionary
private val _dictionary = MutableStateFlow<Dictionary?>(getDictionaies()[1])
fun getTerms(term: String, trans: Translation) {
viewModelScope.launch(Dispatchers.IO) {
var sType = SearchType.START
sType = if (term.length > 2) SearchType.START else SearchType.EXACT
val trans = getTranslationForLanguages("an","sl") ?: return@launch
val transTerms = repository.getTranslationForTerm(term,trans,sType)
_terms.value = transTerms
}
}
fun changeDictionary(dict: Dictionary) {
_dictionary.value = dict
}
fun clearSearch() {
_terms.value = listOf()

View File

@@ -17,6 +17,7 @@ assertjVersion = "3.26.3"
compose-multiplatform = "1.7.0"
coreKtx = "1.9.0"
exposedVersion = "0.55.0"
flagkit = "1.1.0"
flywayVersion = "10.20.1"
h2 = "2.3.232"
hikariCpVersion = "5.1.0"
@@ -67,6 +68,7 @@ lifecycleViewmodelKtx = "2.6.1"
lifecycleViewmodelCompose = "2.8.4"
logback = "1.5.12"
material = "1.7.6"
okio = "3.10.2"
psqlVersion = "42.7.4"
restAssuredVersion = "5.5.0"
runtimeLivedata = "1.7.6"
@@ -89,6 +91,7 @@ composeIcons-linea = { module = "br.com.devsrsouza.compose.icons:linea", version
composeIcons-octicons = { module = "br.com.devsrsouza.compose.icons:octicons", version.ref = "composeIcons" }
composeIcons-simpleIcons = { module = "br.com.devsrsouza.compose.icons:simple-icons", version.ref = "composeIcons" }
composeIcons-tablerIcons = { module = "br.com.devsrsouza.compose.icons:tabler-icons", version.ref = "composeIcons" }
flagkit = { module = "dev.carlsen.flagkit:flagkit", version.ref = "flagkit" }
korau = { module = "com.soywiz.korlibs.korau:korau", version.ref = "korau" }
androidx-core-ktx-v190 = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
@@ -182,6 +185,7 @@ ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "kto
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" }
material = { module = "androidx.compose.material:material", version.ref = "material" }
okio = { module = "com.squareup.okio:okio-bom", version.ref = "okio" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "psqlVersion" }
rest-assured = { module = "io.rest-assured:rest-assured", version.ref = "restAssuredVersion" }
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbolProcessingApi" }

View File

@@ -9,10 +9,12 @@ class LanguageDao(id: EntityID<Int>) : IntEntity(id) {
var name by Languages.name
var shortName by Languages.shortName
var alphaCode by Languages.alphaCode
fun toModel() : Language = Language(
id = id.value,
name = name,
shortName = shortName
shortName = shortName,
alphaCode = alphaCode
)
}

View File

@@ -0,0 +1,28 @@
package db.migration
import dao.DictionaryDao
import kotlinx.serialization.decodeFromString
import tables.*
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import kotlin.system.exitProcess
class V4__add_lang_code: BaseJavaMigration() {
override fun migrate(context: Context?) {
transaction {
val mapOfCodes = mapOf(1 to "SK",2 to "CZ", 3 to "GB",4 to "NL", 5 to "FR", 6 to "DE", 7 to "IT", 8 to "IT",
9 to "HU", 10 to "PL", 11 to "PT", 12 to "RU", 13 to "ES", 14 to "SE" )
SchemaUtils.createMissingTablesAndColumns(Languages)
for (l in Languages.selectAll()) {
val id = l[Languages.id]
Languages.update({ Languages.id eq id}) {
it[alphaCode] = mapOfCodes[id.value]
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
package db.migration
import dao.DictionaryDao
import kotlinx.serialization.decodeFromString
import tables.*
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import kotlin.system.exitProcess
import service.DatabaseFactory
class V5__add_indexes: BaseJavaMigration() {
override fun migrate(context: Context?) {
DatabaseFactory.connectAll()
// for ((id,dict) in DatabaseFactory.getDictionaies()) {
transaction (1) {
SchemaUtils.createMissingTablesAndColumns(Terms)
}
//}
}
}

View File

@@ -9,7 +9,8 @@ import org.jetbrains.exposed.sql.Table
data class Language(
val id: Int,
val name: String,
val shortName: String
val shortName: String,
val alphaCode: String?
)

View File

@@ -13,6 +13,8 @@ import javax.sql.DataSource
import tables.*
import dao.*
import com.typesafe.config.ConfigFactory
import models.Dictionary
import models.Language
import models.Translation
object DatabaseFactory {
@@ -21,7 +23,9 @@ object DatabaseFactory {
private val dbs : MutableMap<Int,Database> = mutableMapOf()
private val hdb : MutableMap<String,Database> = mutableMapOf()
private val htrans: MutableMap<String,Translation> = mutableMapOf()
private val halpha: MutableMap<String,Language> = mutableMapOf()
private val dictById: MutableMap<Int,Dictionary> = mutableMapOf()
private val hLang1: MutableMap<Int,Language> = mutableMapOf()
fun connectAndMigrate() {
log.info("Initialising database")
val pool = hikari()
@@ -43,10 +47,17 @@ object DatabaseFactory {
return HikariDataSource(config)
}
fun getTranslationForLanguages(l1: String, l2: String): Translation? = htrans["${l1}${l2}"]?: null
fun getTranslationForLanguages(l1: String, l2: String): Translation? = htrans["${l1.lowercase()}${l2.lowercase()}"]?: null
fun getLanguageForCode(l: String): Language? = halpha[l.uppercase()]?: null
fun getLanguageForId(id: Int): Language? = hLang1[id]?: null
fun getDictionaies(): Map<Int,Dictionary> = dictById
fun getAllLanguages(): List<Language> {
return halpha.values.toList()
}
fun connectAll() {
if (getDictionaies().any()) return
fun connectAll(): Map<Int,Database> {
val hMap : MutableMap<Int, Database> = mutableMapOf()
transaction {
for (trans in TranslationDao.all()) {
val l1 = trans.lang1.shortName.lowercase()
@@ -55,6 +66,11 @@ object DatabaseFactory {
htrans["${l1}${l2}"] = trans.toModel()
}
for (lang in LanguageDao.all()) {
halpha[lang.shortName] = lang.toModel()
hLang1[lang.id.value] = lang.toModel()
}
for (dict in DictionaryDao.all()) {
val cfg = ConfigFactory.load().getConfig("h2")
val config = HikariConfig().apply {
@@ -68,10 +84,14 @@ object DatabaseFactory {
val db = HikariDataSource(config)
val dbc = Database.connect(db)
dbs[dict.id.value] = dbc
dictById[dict.id.value] = dict.toModel()
hdb["${dict.lang1.shortName.lowercase()}${dict.lang2.shortName.lowercase()}"] = dbc
}
}
return hMap
println("DBS PRINT")
println(dbs)
}
private fun runFlyway(datasource: DataSource) {

View File

@@ -6,4 +6,7 @@ import models.Language
interface LanguageService {
fun getLanguage(id: Int): Language?
fun getAllLanguages(): List<Language>
fun getAlphaCode4String(code: String): Language?
fun getAlphaCode4Lang(lang: Language): Language?
}

View File

@@ -3,6 +3,7 @@ package service
import dao.LanguageDao
import org.jetbrains.exposed.sql.transactions.transaction
import service.DatabaseFactory.dbExecId
import service.DatabaseFactory.getLanguageForCode
import models.Language
class LanguageServiceImpl : LanguageService {
@@ -14,4 +15,12 @@ class LanguageServiceImpl : LanguageService {
LanguageDao.all().map { it.toModel() }
}
override fun getAlphaCode4String(code: String): Language? {
return getLanguageForCode(code)
}
override fun getAlphaCode4Lang(lang: Language): Language? {
return getLanguageForCode(lang.shortName)
}
}

View File

@@ -47,7 +47,7 @@ class TermServiceImpl : TermService {
if (type == SearchType.EXACT)
TermDao.find { Terms.string2 eq s }.map { it.toFullModel() }
else
TermDao.find { Terms.string1 like s }.map { it.toFullModel() }
TermDao.find { Terms.string2 like s }.map { it.toFullModel() }
}
}

View File

@@ -8,5 +8,5 @@ object Languages : IntIdTable() {
val name = varchar("name", 255)
val shortName = varchar("short_name", 255)
val alphaCode = char("alpha_code",2).nullable()
}

View File

@@ -6,8 +6,8 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import tables.Suffixes
object Terms : IntIdTable() {
val string1 = varchar("string1", 255)
val string2 = varchar("string2", 255)
val string1 = varchar("string1", 255).index()
val string2 = varchar("string2", 255).index()
val suffix1 = reference("suffix1_id",Suffixes).nullable()
val suffix2 = reference("suffix2_id",Suffixes).nullable()
val type = reference("type_id",DictTypes).nullable()