diff --git a/api/src/main/java/data/dto/DefaultDictionaryDto.kt b/api/src/main/java/data/dto/DefaultDictionaryDto.kt new file mode 100644 index 0000000..5aa473d --- /dev/null +++ b/api/src/main/java/data/dto/DefaultDictionaryDto.kt @@ -0,0 +1,17 @@ +package data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class DefaultDictionaryDto( + val id: String, + val name: String, + val description: String, + val terms: List, +) + +@Serializable +data class DefaultTermDto( + val name: String, + val description: String, +) diff --git a/api/src/main/java/data/dto/DictionaryCollectionDto.kt b/api/src/main/java/data/dto/DictionaryCollectionDto.kt new file mode 100644 index 0000000..e2be85c --- /dev/null +++ b/api/src/main/java/data/dto/DictionaryCollectionDto.kt @@ -0,0 +1,10 @@ +package data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class DictionaryCollectionDto( + val id: String, + val name: String, + val dictionaryCount: Int, +) diff --git a/app/src/main/java/com/example/aftermathandroid/presentation/navigation/dictionary/DictionaryNavigation.kt b/app/src/main/java/com/example/aftermathandroid/presentation/navigation/dictionary/DictionaryNavigation.kt index 2fbd673..c638233 100644 --- a/app/src/main/java/com/example/aftermathandroid/presentation/navigation/dictionary/DictionaryNavigation.kt +++ b/app/src/main/java/com/example/aftermathandroid/presentation/navigation/dictionary/DictionaryNavigation.kt @@ -36,10 +36,14 @@ fun DictionaryNavigation( rootViewModel.back() } ) { navController -> - NavHost(navController = navController, + NavHost( + navController = navController, startDestination = DictionaryRoute.Menu.path, enterTransition = { fadeIn() }, - exitTransition = { fadeOut() }) { + exitTransition = { fadeOut() }, + popEnterTransition = { fadeIn() }, + popExitTransition = { fadeOut() }, + ) { composable(DictionaryRoute.Menu.path) { DictionariesMenuScreen( dictionaryScreenSource = dictionaryScreenSource, dictionaryNavigation = viewModel diff --git a/app/src/main/java/com/example/aftermathandroid/presentation/navigation/root/RootNavigation.kt b/app/src/main/java/com/example/aftermathandroid/presentation/navigation/root/RootNavigation.kt index cb57ef4..91541f7 100644 --- a/app/src/main/java/com/example/aftermathandroid/presentation/navigation/root/RootNavigation.kt +++ b/app/src/main/java/com/example/aftermathandroid/presentation/navigation/root/RootNavigation.kt @@ -1,11 +1,7 @@ package com.example.aftermathandroid.presentation.navigation.root -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -52,15 +48,11 @@ fun RootNavigation( || navController.currentDestination?.route == RootDestination.Registration.path if (it == null && !isAuthDestination) { navController.navigate(RootDestination.Login.path) { - popUpTo(RootDestination.Home.path) { - inclusive = true - } + popUpTo(0) } } else if (it != null && isAuthDestination) { navController.navigate(RootDestination.Home.path) { - popUpTo(RootDestination.Home.path) { - inclusive = true - } + popUpTo(0) } } } @@ -69,9 +61,10 @@ fun RootNavigation( NavHost( navController = navController, startDestination = RootDestination.Home.path, - enterTransition = { fadeIn() }, - exitTransition = { fadeOut() }, - popExitTransition = { fadeOut() }, + enterTransition = { slideInHorizontally { it } }, + exitTransition = { slideOutHorizontally { -it / 2 } }, + popEnterTransition = { slideInHorizontally { -it / 2 } }, + popExitTransition = { slideOutHorizontally { it } }, ) { composable(RootDestination.Profile.path) { ProfileScreen() @@ -84,9 +77,6 @@ fun RootNavigation( } composable( RootDestination.Registration.path, - enterTransition = { slideInHorizontally { it } }, - popExitTransition = { slideOutHorizontally { it } }, - exitTransition = { slideOutHorizontally { it } }, ) { RegisterScreen() } @@ -102,9 +92,6 @@ fun RootNavigation( } composable( RootDestination.SelectDictionary.path, - enterTransition = { slideInVertically(initialOffsetY = { it }) }, - exitTransition = { slideOutVertically(targetOffsetY = { it }) }, - popExitTransition = { slideOutVertically(targetOffsetY = { it }) }, ) { DictionarySelectScreen() } diff --git a/server/src/main/kotlin/com/krushiler/data/repository/DictionaryRepository.kt b/server/src/main/kotlin/com/krushiler/data/repository/DictionaryRepository.kt index 1873930..f734e79 100644 --- a/server/src/main/kotlin/com/krushiler/data/repository/DictionaryRepository.kt +++ b/server/src/main/kotlin/com/krushiler/data/repository/DictionaryRepository.kt @@ -3,14 +3,20 @@ package com.krushiler.data.repository import com.krushiler.data.storage.dao.DictionaryDao import com.krushiler.data.storage.dao.UserDao import com.krushiler.data.storage.dbo.ChangeTermDboAction -import domain.model.DictionarySearchData import com.krushiler.domain.model.PagingData +import data.dto.DefaultDictionaryDto import data.dto.DictionaryDto import data.dto.DictionaryInfoDto import data.dto.TermDto import data.dto.UserDto import data.response.PagedResponse +import domain.model.DictionarySearchData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import util.generateUUID +import java.io.BufferedReader +import java.io.InputStreamReader class DictionaryRepository(private val dictionaryDao: DictionaryDao, private val userDao: UserDao) { suspend fun getUserDictionaries(user: UserDto, pagingData: PagingData): PagedResponse { @@ -27,11 +33,12 @@ class DictionaryRepository(private val dictionaryDao: DictionaryDao, private val pagingData: PagingData, searchData: DictionarySearchData, userId: String?, + collectionId: String? ): PagedResponse { - val dictionaries = dictionaryDao.getDictionaries(pagingData, searchData) + val dictionaries = dictionaryDao.getDictionaries(pagingData, searchData, collectionId) return PagedResponse( items = dictionaries.items.map { - val userDbo = userDao.getUserByLogin(it.authorId) + val userDbo = it.authorId?.let { authorId -> userDao.getUserByLogin(authorId) } val user = userDbo?.let { dbo -> UserDto( dbo.login, dbo.name, dbo.avatar @@ -49,7 +56,7 @@ class DictionaryRepository(private val dictionaryDao: DictionaryDao, private val val dictionary = dictionaryDao.getDictionary(id) val terms = dictionaryDao.getTerms(id) return dictionary?.let { - val userDbo = userDao.getUserByLogin(it.authorId) + val userDbo = it.authorId?.let { authorId -> userDao.getUserByLogin(authorId) } val user = userDbo?.let { dbo -> UserDto( dbo.login, dbo.name, dbo.avatar @@ -96,4 +103,62 @@ class DictionaryRepository(private val dictionaryDao: DictionaryDao, private val if (this == null || userId == null) return false return this == userId } + + private val jsonFormat = Json { ignoreUnknownKeys = true } + + + suspend fun createInitialDictionaries() { + val collectionId = "default" + val collection = dictionaryDao.getDictionaryCollection(collectionId) + if (collection != null) return + + dictionaryDao.createDictionaryCollection( + id = collectionId, + name = "Default" + ) + + try { + val id = createDictionaryFromResource("dictionaries/kotlin.json") + if (id != null) { + dictionaryDao.addDictionaryToCollection(id, collectionId) + } + } catch (_: Exception) { + } + } + + private suspend fun createDictionaryFromResource(resourceName: String): String? { + val inputStream = object {}.javaClass.classLoader.getResourceAsStream(resourceName) + + if (inputStream != null) { + val reader = BufferedReader(InputStreamReader(inputStream)) + val content = reader.readText() + withContext(Dispatchers.IO) { + reader.close() + inputStream.close() + } + + val json = jsonFormat.decodeFromString(content) + + if (dictionaryDao.getDictionary(json.id) != null) return null + + dictionaryDao.createDictionary( + id = json.id, + authorId = null, + name = json.name, + description = json.description, + ) + + json.terms.forEach { + dictionaryDao.createTerm( + id = generateUUID(), + dictionaryId = json.id, + term = it.name, + description = it.description + ) + } + + return json.id + } + return null + } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/krushiler/data/storage/dao/DictionaryDao.kt b/server/src/main/kotlin/com/krushiler/data/storage/dao/DictionaryDao.kt index 20f950b..112458a 100644 --- a/server/src/main/kotlin/com/krushiler/data/storage/dao/DictionaryDao.kt +++ b/server/src/main/kotlin/com/krushiler/data/storage/dao/DictionaryDao.kt @@ -4,13 +4,17 @@ import com.krushiler.data.dao.Dao import com.krushiler.data.storage.database.DatabaseList import com.krushiler.data.storage.dbo.ChangeTermDboAction import com.krushiler.data.storage.dbo.Dictionaries +import com.krushiler.data.storage.dbo.DictionaryCollectionDbo +import com.krushiler.data.storage.dbo.DictionaryCollectionDictionaries +import com.krushiler.data.storage.dbo.DictionaryCollections import com.krushiler.data.storage.dbo.DictionaryDbo import com.krushiler.data.storage.dbo.TermDbo import com.krushiler.data.storage.dbo.Terms -import domain.model.DictionarySearchData import com.krushiler.domain.model.PagingData +import domain.model.DictionarySearchData import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert @@ -18,6 +22,7 @@ import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.update +import util.generateUUID class DictionaryDao(database: Database) : Dao(database) { suspend fun getUserDictionaries(userId: String, pagingData: PagingData): DatabaseList = dbQuery { @@ -32,6 +37,7 @@ class DictionaryDao(database: Database) : Dao(database) { suspend fun getDictionaries( pagingData: PagingData, searchData: DictionarySearchData, + collectionId: String? ): DatabaseList = dbQuery { val request = Dictionaries.selectAll() if (searchData.authors.isNotEmpty()) { @@ -51,6 +57,15 @@ class DictionaryDao(database: Database) : Dao(database) { } } } + if (collectionId != null) { + request.andWhere { + Dictionaries.id inList + DictionaryCollectionDictionaries.select { DictionaryCollectionDictionaries.collectionId eq collectionId } + .map { + it[DictionaryCollectionDictionaries.dictionaryId] + } + } + } DatabaseList( items = request.copy().limit( pagingData.limit, pagingData.offset.toLong() @@ -67,7 +82,7 @@ class DictionaryDao(database: Database) : Dao(database) { Terms.select { Terms.dictionaryId eq dictionaryId }.map { Terms.resultRowToTerm(it) } } - suspend fun createDictionary(id: String, authorId: String, name: String, description: String): Boolean = dbQuery { + suspend fun createDictionary(id: String, authorId: String?, name: String, description: String): Boolean = dbQuery { Dictionaries.insert { it[Dictionaries.name] = name it[Dictionaries.description] = description @@ -119,4 +134,62 @@ class DictionaryDao(database: Database) : Dao(database) { it[Terms.description] = description } > 0 } + + suspend fun createDictionaryCollection( + id: String, + name: String, + ) = dbQuery { + DictionaryCollections.insert { + it[DictionaryCollections.id] = id + it[DictionaryCollections.name] = name + } + } + + suspend fun addDictionaryToCollection( + dictionaryId: String, + collectionId: String, + ) = dbQuery { + DictionaryCollectionDictionaries.insert { + it[DictionaryCollectionDictionaries.id] = generateUUID() + it[DictionaryCollectionDictionaries.dictionaryId] = dictionaryId + it[DictionaryCollectionDictionaries.collectionId] = collectionId + } + } + + suspend fun removeDictionaryFromCollection( + dictionaryId: String, + collectionId: String, + ) = dbQuery { + DictionaryCollections.deleteWhere { + (DictionaryCollectionDictionaries.dictionaryId eq dictionaryId).and { DictionaryCollectionDictionaries.collectionId eq collectionId } + } + } + + suspend fun getDictionaryCollections( + pagingData: PagingData + ): DatabaseList = dbQuery { + val request = DictionaryCollections.selectAll() + DatabaseList( + items = request.copy().limit(pagingData.limit, pagingData.offset.toLong()) + .map { DictionaryCollections.resultRowToDictionaryCollection(it) }, + total = request.copy().count().toInt(), + ) + } + + suspend fun getDictionaryCollection( + id: String + ): DictionaryCollectionDbo? = dbQuery { + DictionaryCollections.select { DictionaryCollections.id eq id } + .firstOrNull()?.let { DictionaryCollections.resultRowToDictionaryCollection(it) } + } + + suspend fun getDictionaryCollectionDictionaries( + collectionId: String, + ): List = dbQuery { + DictionaryCollectionDictionaries.select { DictionaryCollectionDictionaries.collectionId eq collectionId }.map { + Dictionaries.select { + Dictionaries.id eq it[DictionaryCollectionDictionaries.dictionaryId] + }.map { Dictionaries.resultRowToDictionary(it) }.firstOrNull() + }.mapNotNull { it } + } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryCollectionDbo.kt b/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryCollectionDbo.kt new file mode 100644 index 0000000..bead1ce --- /dev/null +++ b/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryCollectionDbo.kt @@ -0,0 +1,29 @@ +package com.krushiler.data.storage.dbo + +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table + +data class DictionaryCollectionDbo( + val id: String, + val name: String, +) + +object DictionaryCollections : Table() { + val id = varchar("id", 128) + val name = varchar("name", 128) + + override val primaryKey = PrimaryKey(id) + + fun resultRowToDictionaryCollection(row: ResultRow) = DictionaryCollectionDbo( + id = row[id], + name = row[name], + ) +} + +object DictionaryCollectionDictionaries : Table() { + val id = varchar("id", 128) + val dictionaryId = varchar("dictionaryId", 128).references(Dictionaries.id) + val collectionId = varchar("collectionId", 128).references(DictionaryCollections.id) + + override val primaryKey = PrimaryKey(id) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryDbo.kt b/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryDbo.kt index a70a631..3c43d38 100644 --- a/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryDbo.kt +++ b/server/src/main/kotlin/com/krushiler/data/storage/dbo/DictionaryDbo.kt @@ -5,14 +5,14 @@ import org.jetbrains.exposed.sql.Table data class DictionaryDbo( val id: String, - val authorId: String, + val authorId: String?, val name: String, val description: String, ) object Dictionaries : Table() { val id = varchar("id", 128) - val authorId = varchar("authorId", 128) + val authorId = varchar("authorId", 128).nullable() val name = varchar("name", 128) val description = varchar("description", 128) diff --git a/server/src/main/kotlin/com/krushiler/domain/interactor/DictionaryInteractor.kt b/server/src/main/kotlin/com/krushiler/domain/interactor/DictionaryInteractor.kt index 4ef1288..b810c5d 100644 --- a/server/src/main/kotlin/com/krushiler/domain/interactor/DictionaryInteractor.kt +++ b/server/src/main/kotlin/com/krushiler/domain/interactor/DictionaryInteractor.kt @@ -49,7 +49,10 @@ class DictionaryInteractor( pagingData: PagingData, searchData: DictionarySearchData, userId: String?, + collectionId: String? ): PagedResponse { - return dictionaryRepository.getDictionaries(pagingData, searchData, userId) + return dictionaryRepository.getDictionaries(pagingData, searchData, userId, collectionId) } + + suspend fun createInitialDictionaries() = dictionaryRepository.createInitialDictionaries() } \ No newline at end of file diff --git a/server/src/main/kotlin/com/krushiler/plugins/Di.kt b/server/src/main/kotlin/com/krushiler/plugins/Di.kt index 2487471..a15dbea 100644 --- a/server/src/main/kotlin/com/krushiler/plugins/Di.kt +++ b/server/src/main/kotlin/com/krushiler/plugins/Di.kt @@ -1,12 +1,20 @@ package com.krushiler.plugins import com.krushiler.di.dataModule +import com.krushiler.domain.interactor.DictionaryInteractor import io.ktor.server.application.Application import io.ktor.server.application.install +import kotlinx.coroutines.launch +import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin fun Application.configureDi() { install(Koin) { modules(dataModule) } + + launch { + val dictionaryInteractor: DictionaryInteractor by inject() + dictionaryInteractor.createInitialDictionaries() + } } \ No newline at end of file diff --git a/server/src/main/resources/dictionaries/kotlin.json b/server/src/main/resources/dictionaries/kotlin.json new file mode 100644 index 0000000..05e57a3 --- /dev/null +++ b/server/src/main/resources/dictionaries/kotlin.json @@ -0,0 +1,127 @@ +{ + "id": "kotlin-0", + "name": "Kotlin", + "description": "A dictionary with Kotlin terms", + "terms": [ + { + "name": "Lock", + "description": "Ensures exclusive access to a shared resource during multithreading" + }, + { + "name": "Mutex", + "description": "Fundamental synchronization mechanism for controlling access to shared resources" + }, + { + "name": "let", + "description": "Facilitates the execution of a code block on a non-null object" + }, + { + "name": "Coroutine", + "description": "Facilitates asynchronous programming by suspending and resuming computations" + }, + { + "name": "Smart Cast", + "description": "Implicit casting of types after a check or comparison" + }, + { + "name": "Destructuring Declarations", + "description": "Facilitates the extraction of components from data structures" + }, + { + "name": "Nullable Type", + "description": "Denotes a type that can hold a null reference" + }, + { + "name": "Companion Object", + "description": "Object associated with a class rather than an instance" + }, + { + "name": "Extension Function", + "description": "Enables adding new functions to existing classes without inheritance" + }, + { + "name": "Immutable", + "description": "Denotes objects or data structures that cannot be modified after creation" + }, + { + "name": "Lambda Expression", + "description": "Anonymous function or code block used for concise syntax" + }, + { + "name": "High-Order Function", + "description": "Function that takes other functions as parameters or returns them" + }, + { + "name": "Reified Type", + "description": "Type with actual class information available at runtime" + }, + { + "name": "Sealed Class", + "description": "Restricts the inheritance hierarchy to a defined set of classes" + }, + { + "name": "Suspension Function", + "description": "Function that can be paused and later resumed" + }, + { + "name": "Property Delegation", + "description": "Delegating the implementation of a property to another object" + }, + { + "name": "Data Class", + "description": "Class specifically designed for holding data with auto-generated methods" + }, + { + "name": "Lateinit", + "description": "Indicates that a variable will be initialized later in the code" + }, + { + "name": "Tail Recursion", + "description": "Optimization technique for recursive functions to improve performance" + }, + { + "name": "Default Argument", + "description": "Function parameter with a predefined value if not specified" + }, + { + "name": "Range", + "description": "Represents a sequence of values between two endpoints" + }, + { + "name": "Type Alias", + "description": "Alternative name for an existing type for improved readability" + }, + { + "name": "Inline Function", + "description": "Function whose code is inserted at the call site during compilation" + }, + { + "name": "Operator Overloading", + "description": "Assigning custom behavior to operators for user-defined types" + }, + { + "name": "Enum Class", + "description": "Type that represents a fixed set of constants" + }, + { + "name": "Platform-Specific Code", + "description": "Code tailored for a specific platform, such as JVM or Android" + }, + { + "name": "Function Reference", + "description": "Referring to functions by name or using a callable reference" + }, + { + "name": "Type Inference", + "description": "Compiler's ability to deduce variable types without explicit declarations" + }, + { + "name": "Where Clause", + "description": "Clause used in generics to impose constraints on type parameters" + }, + { + "name": "Init Block", + "description": "Block of code executed during the initialization of an instance" + } + ] +} \ No newline at end of file