feat: Blogs

This commit is contained in:
sweetbread 2024-03-20 21:14:31 +03:00
parent 0d4ba49111
commit 040474cc1d
7 changed files with 382 additions and 36 deletions

View File

@ -84,6 +84,7 @@ dependencies {
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.logging) implementation(libs.ktor.client.logging)
implementation(libs.coil.compose)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.splitties.funpack.android.base.with.views.dsl) implementation(libs.splitties.funpack.android.base.with.views.dsl)

View File

@ -2,23 +2,29 @@ package ru.sweetbread.unn.ui
import io.ktor.client.request.forms.submitForm import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.parameters import io.ktor.http.parameters
import org.apache.commons.text.StringEscapeUtils
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.ui.layout.LoginData import ru.sweetbread.unn.ui.layout.LoginData
import ru.sweetbread.unn.ui.layout.client import ru.sweetbread.unn.ui.layout.client
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
private lateinit var PHPSESSID: String private lateinit var PHPSESSID: String
private lateinit var CSRF: String
lateinit var ME: User lateinit var ME: User
const val portalURL = "https://portal.unn.ru" const val portalURL = "https://portal.unn.ru"
const val ruzapiURL = "$portalURL/ruzapi" const val ruzapiURL = "$portalURL/ruzapi"
const val vuzapiURL = "$portalURL/bitrix/vuz/api"
const val restURL = "$portalURL/rest"
enum class Type(val s: String) { enum class Type(val s: String) {
Student("student"), Student("student"),
@ -64,15 +70,31 @@ class KindOfWork( val name: String,
class Lecturer( val name: String, class Lecturer( val name: String,
val rank: LecturerRank, val rank: LecturerRank,
val email: String, val email: String,
val oid: Int, val unnId: Int,
val uid: String) val uid: String)
class User (val id: String, class User (val unnId: Int?,
val uns: String, val bitrixId: Int,
val userId: Int,
val type: Type, val type: Type,
val email: String, val email: String,
val name: String, val nameRu: String,
val info: String) val nameEn: String,
val isMale: Boolean,
val birthday: LocalDate,
val avatar: ImageSet)
class Post(
val id: Int,
val authorId: Int,
val enableComments: Boolean,
val numComments: Int,
val date: LocalDateTime,
val content: String)
class ImageSet(val original: String,
val thumbnail: String,
val small: String)
/** /**
* Authorize user by [login] and [password] * Authorize user by [login] and [password]
@ -96,6 +118,7 @@ suspend fun auth(login: String = LoginData.login, password: String = LoginData.p
if (r.status.value == 302) { if (r.status.value == 302) {
PHPSESSID = """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] PHPSESSID = """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
getMyself(login) getMyself(login)
getCSRF()
return true return true
} }
return false return false
@ -105,24 +128,48 @@ suspend fun auth(login: String = LoginData.login, password: String = LoginData.p
* Save info about current [User] in memory * Save info about current [User] in memory
*/ */
private suspend fun getMyself(login: String) { private suspend fun getMyself(login: String) {
val r = client.get("$ruzapiURL/studentinfo") { val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo") {
parameter("uns", login.substring(1)) parameter("uns", login.substring(1))
} }.bodyAsText())
val json = JSONObject(r.bodyAsText())
val user = JSONObject(
client.get("$vuzapiURL/user") {
header("Cookie", "PHPSESSID=${PHPSESSID}")
}.bodyAsText()
)
ME = User( ME = User(
id = json.getString("id"), unnId = studentinfo.getString("id").toInt(),
uns = json.getString("uns"), bitrixId = user.getInt("bitrix_id"),
type = when(json.getString("type")) { userId = user.getInt("id"),
type = when(studentinfo.getString("type")) {
"lecturer" -> Type.Lecturer // ig,,, "lecturer" -> Type.Lecturer // ig,,,
else -> Type.Student else -> Type.Student
}, },
email = json.getString("email"), email = user.getString("email"),
name = json.getString("fio"), nameRu = user.getString("fullname"),
info = json.getString("info") nameEn = user.getString("fullname_en"),
isMale = user.getString("sex") == "M",
birthday = LocalDate.parse(
user.getString("birthdate"),
DateTimeFormatter.ofPattern("yyyy-MM-dd")
),
avatar = user.getJSONObject("photo").let {
ImageSet(
it.getString("orig"),
it.getString("thumbnail"),
it.getString("small"),
)
}
) )
} }
suspend fun getSchedule(type: Type = ME.type, id: String = ME.id, start: LocalDate, finish: LocalDate): ArrayList<ScheduleUnit> { suspend fun getSchedule(
type: Type = ME.type,
id: Int = ME.unnId!!,
start: LocalDate,
finish: LocalDate
): ArrayList<ScheduleUnit> {
val unnDatePattern = DateTimeFormatter.ofPattern("yyyy.MM.dd") val unnDatePattern = DateTimeFormatter.ofPattern("yyyy.MM.dd")
val r = client.get("$ruzapiURL/schedule/${type.s}/$id") { val r = client.get("$ruzapiURL/schedule/${type.s}/$id") {
@ -144,7 +191,7 @@ suspend fun getSchedule(type: Type = ME.type, id: String = ME.id, start: LocalDa
Lecturer( Lecturer(
name = lecturer.getString("lecturer"), name = lecturer.getString("lecturer"),
email = lecturer.getString("lecturerEmail"), email = lecturer.getString("lecturerEmail"),
oid = lecturer.getInt("lecturerOid"), unnId = lecturer.getInt("lecturerOid"),
uid = lecturer.getString("lecturerUID"), uid = lecturer.getString("lecturerUID"),
rank = when (lecturer.getString("lecturer_rank")) { rank = when (lecturer.getString("lecturer_rank")) {
"АССИСТ" -> LecturerRank.Assistant "АССИСТ" -> LecturerRank.Assistant
@ -190,3 +237,79 @@ suspend fun getSchedule(type: Type = ME.type, id: String = ME.id, start: LocalDa
} }
return out return out
} }
suspend fun getCSRF() {
val r = client.get("$restURL/log.blogpost.get") {
header("Cookie", "PHPSESSID=${PHPSESSID}")
parameter("sessid", "")
}
CSRF = JSONObject(r.bodyAsText()).getString("sessid")
}
suspend fun getBlogposts(): ArrayList<Post> {
val r = client.get("$restURL/log.blogpost.get") {
header("Cookie", "PHPSESSID=${PHPSESSID}")
parameter("sessid", CSRF)
}
val json = JSONObject(r.bodyAsText())
val result = json.getJSONArray("result")
val out = arrayListOf<Post>()
for (i in 0 until result.length()) {
val el = result.getJSONObject(i)
out.add(
Post(
id = el.getString("ID").toInt(),
authorId = el.getString("AUTHOR_ID").toInt(),
enableComments = el.getString("ENABLE_COMMENTS") == "Y",
numComments = el.getString("NUM_COMMENTS").toInt(),
date = LocalDateTime.parse(
el.getString("DATE_PUBLISH"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+03:00'")
),
content = StringEscapeUtils.escapeHtml4(el.getString("DETAIL_TEXT"))
)
)
}
return out
}
suspend fun getUserByBitrixId(id: Int): User {
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
header("Cookie", "PHPSESSID=${PHPSESSID}")
}.bodyAsText()).getInt("id")
return getUser(userId)
}
suspend fun getUser(id: Int): User {
val json = JSONObject(
client.get("$vuzapiURL/user/$id") {
header("Cookie", "PHPSESSID=${PHPSESSID}")
}.bodyAsText()
)
return User(
unnId = null,
bitrixId = json.getInt("bitrix_id"),
userId = json.getInt("id"),
type = when (json.getJSONArray("profiles").getJSONObject(0).getString("type")) {
"lecturer" -> Type.Lecturer // ig,,,
else -> Type.Student
},
email = json.getString("email"),
nameRu = json.getString("fullname"),
nameEn = json.getString("fullname_en"),
isMale = json.getString("sex") == "M",
birthday = LocalDate.parse(
json.getString("birthdate"),
DateTimeFormatter.ofPattern("yyyy-MM-dd")
),
avatar = json.getJSONObject("photo").let {
ImageSet(
it.getString("orig"),
it.getString("thumbnail"),
it.getString("small"),
)
}
)
}

View File

@ -0,0 +1,216 @@
package ru.sweetbread.unn.ui.composes
import android.util.Log
import androidx.compose.foundation.background
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.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import ru.sweetbread.unn.R
import ru.sweetbread.unn.ui.ImageSet
import ru.sweetbread.unn.ui.Post
import ru.sweetbread.unn.ui.Type
import ru.sweetbread.unn.ui.User
import ru.sweetbread.unn.ui.getBlogposts
import ru.sweetbread.unn.ui.getUserByBitrixId
import ru.sweetbread.unn.ui.portalURL
import ru.sweetbread.unn.ui.theme.UNNTheme
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
val defUser = User(
null,
123,
123,
Type.Student,
"cool.email@domain.com",
"Джон Сигма Омегович",
"Jon Sigma Omega",
true,
LocalDate.now(),
ImageSet(
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg"
)
)
@Composable
fun Blogposts(viewModel: PostViewModel = viewModel()) {
val posts by viewModel.posts.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadPosts()
}
if (posts.isNotEmpty()) {
Log.d("Another fuck", posts.size.toString())
LazyColumn {
items(posts) {
PostItem(
Modifier
.padding(8.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondaryContainer),
post = it
)
}
}
} else {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
trackColor = MaterialTheme.colorScheme.secondary,
)
}
}
class PostRepository {
suspend fun loadPosts(): List<Post> {
return getBlogposts()
}
}
class PostViewModel : ViewModel() {
private val repository = PostRepository()
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
suspend fun loadPosts() {
_posts.value = repository.loadPosts()
}
}
@Composable
@NonRestartableComposable
fun UserItem(modifier: Modifier = Modifier, user: User, info: String? = null) {
Row(
modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
AsyncImage(
modifier = Modifier
.padding(end = 8.dp)
.size(48.dp)
.clip(RoundedCornerShape(50)),
model = portalURL + user.avatar.thumbnail,
contentDescription = user.nameEn
)
Column {
Text(user.nameRu, fontWeight = FontWeight.Bold)
if (!info.isNullOrBlank())
Text(
text = info,
fontStyle = FontStyle.Italic,
fontSize = MaterialTheme.typography.labelLarge.fontSize
)
}
}
}
@Composable
@NonRestartableComposable
fun PostItem(modifier: Modifier = Modifier, post: Post) {
var user: User? by remember { mutableStateOf(null) }
LaunchedEffect(post) {
user = getUserByBitrixId(post.authorId)
}
Column(modifier.padding(16.dp)) {
Log.d("FUUUUUUUUUUUCK", user.toString())
if (user != null)
UserItem(user = user!!)
else
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
trackColor = MaterialTheme.colorScheme.secondary,
)
Text(text = post.content)
Text(text = post.date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)))
}
}
@Preview
@Composable
fun UserItemPreview() {
UNNTheme {
Surface {
UserItem(
Modifier
.width(300.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primaryContainer), defUser, Type.Student.s
)
}
}
}
@Preview
@Composable
fun PostItemPreview() {
val post = Post(
id = 154923,
authorId = 165945,
enableComments = true,
numComments = 0,
date = LocalDateTime.of(2024, 3, 20, 18, 55, 20),
content = stringResource(id = R.string.lorem)
)
UNNTheme {
Surface {
PostItem(
Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primaryContainer), post
)
}
}
}

View File

@ -7,12 +7,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -23,7 +20,6 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -45,7 +41,6 @@ import com.kizitonwose.calendar.compose.WeekCalendar
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.intellij.lang.annotations.JdkConstants.HorizontalAlignment
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.ui.Auditorium import ru.sweetbread.unn.ui.Auditorium
import ru.sweetbread.unn.ui.Building import ru.sweetbread.unn.ui.Building
@ -290,7 +285,7 @@ fun ScheduleItemPreview() {
name = "Фамилия Имя Отчество", name = "Фамилия Имя Отчество",
rank = LecturerRank.SLecturer, rank = LecturerRank.SLecturer,
email = "", email = "",
oid = 28000, unnId = 28000,
uid = "51000" uid = "51000"
) )
), ),
@ -338,7 +333,7 @@ fun ScheduleExpandedItemPreview() {
name = "Фамилия Имя Отчество", name = "Фамилия Имя Отчество",
rank = LecturerRank.SLecturer, rank = LecturerRank.SLecturer,
email = "", email = "",
oid = 28000, unnId = 28000,
uid = "51000" uid = "51000"
) )
), ),

View File

@ -17,7 +17,6 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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
@ -33,9 +32,11 @@ import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import ru.sweetbread.unn.ui.composes.Blogposts
import ru.sweetbread.unn.ui.composes.Schedule import ru.sweetbread.unn.ui.composes.Schedule
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.toast.toast import splitties.toast.toast
import java.io.File
val client = HttpClient { val client = HttpClient {
install(HttpCache) install(HttpCache)
@ -59,34 +60,41 @@ val client = HttpClient {
} }
} }
val cacheDir = File("/data/data/ru.sweetbread.unn/files/cache")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d("mkdir", cacheDir.mkdir().toString())
setContent { setContent {
UNNTheme { UNNTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
val navController = rememberNavController() val navController = rememberNavController()
var route by remember { mutableStateOf("home") } var route by remember { mutableStateOf("portal/blogposts") }
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar { NavigationBar {
NavigationBarItem( NavigationBarItem(
onClick = { toast("Not implemented") }, onClick = {
route = "portal/blogposts"
navController.navigate(route)
},
icon = { icon = {
Icon( Icon(
Icons.Filled.Home, Icons.Filled.Home,
contentDescription = "Home" contentDescription = "Home"
) )
}, },
selected = route.startsWith("home") selected = route.startsWith("portal/")
) )
NavigationBarItem( NavigationBarItem(
onClick = { onClick = {
navController.navigate("journal/schedule") route = "journal/schedule"
route = "journal/schedule" }, navController.navigate(route)
},
icon = { icon = {
Icon( Icon(
Icons.Filled.DateRange, Icons.Filled.DateRange,
@ -110,9 +118,9 @@ class MainActivity : ComponentActivity() {
} }
) {innerPadding -> ) {innerPadding ->
Box(Modifier.padding(innerPadding)) { Box(Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = "home/blogposts") { NavHost(navController, startDestination = "portal/blogposts") {
composable("home/blogposts") { composable("portal/blogposts") {
Text("Not implemented") Blogposts()
} }
composable("journal/schedule") { composable("journal/schedule") {
Schedule() Schedule()

View File

@ -12,5 +12,6 @@
<string name="auditorium">Auditorium</string> <string name="auditorium">Auditorium</string>
<string name="building">Building</string> <string name="building">Building</string>
<string name="floor">Floor</string> <string name="floor">Floor</string>
<string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string>
<!-- <string name="login_failed">"Login failed"</string>--> <!-- <string name="login_failed">"Login failed"</string>-->
</resources> </resources>

View File

@ -1,6 +1,7 @@
[versions] [versions]
acraHttp = "5.11.3" acraHttp = "5.11.3"
agp = "8.3.1" agp = "8.3.1"
coilCompose = "2.6.0"
compose = "2.5.0" compose = "2.5.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
datastorePreferences = "1.0.0" datastorePreferences = "1.0.0"
@ -33,6 +34,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
compose = { module = "com.kizitonwose.calendar:compose", version.ref = "compose" } compose = { module = "com.kizitonwose.calendar:compose", version.ref = "compose" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }