diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8cab3a6..9045aed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) implementation(libs.ktor.client.logging) + implementation(libs.coil.compose) implementation(libs.androidx.datastore.preferences) implementation(libs.splitties.funpack.android.base.with.views.dsl) diff --git a/app/src/main/java/ru/sweetbread/unn/ui/API.kt b/app/src/main/java/ru/sweetbread/unn/ui/API.kt index bfa7835..e08986c 100644 --- a/app/src/main/java/ru/sweetbread/unn/ui/API.kt +++ b/app/src/main/java/ru/sweetbread/unn/ui/API.kt @@ -2,23 +2,29 @@ package ru.sweetbread.unn.ui import io.ktor.client.request.forms.submitForm import io.ktor.client.request.get +import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.client.statement.bodyAsText import io.ktor.http.parameters +import org.apache.commons.text.StringEscapeUtils import org.json.JSONArray import org.json.JSONObject import ru.sweetbread.unn.R import ru.sweetbread.unn.ui.layout.LoginData import ru.sweetbread.unn.ui.layout.client import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime import java.time.format.DateTimeFormatter private lateinit var PHPSESSID: String +private lateinit var CSRF: String lateinit var ME: User const val portalURL = "https://portal.unn.ru" const val ruzapiURL = "$portalURL/ruzapi" +const val vuzapiURL = "$portalURL/bitrix/vuz/api" +const val restURL = "$portalURL/rest" enum class Type(val s: String) { Student("student"), @@ -61,18 +67,34 @@ class KindOfWork( val name: String, val uid: String, val complexity: Int) -class Lecturer( val name: String, - val rank: LecturerRank, - val email: String, - val oid: Int, - val uid: String) +class Lecturer( val name: String, + val rank: LecturerRank, + val email: String, + val unnId: Int, + val uid: String) -class User (val id: String, - val uns: String, +class User (val unnId: Int?, + val bitrixId: Int, + val userId: Int, val type: Type, val email: String, - val name: String, - val info: String) + val nameRu: 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] @@ -96,6 +118,7 @@ suspend fun auth(login: String = LoginData.login, password: String = LoginData.p if (r.status.value == 302) { PHPSESSID = """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] getMyself(login) + getCSRF() return true } return false @@ -105,24 +128,48 @@ suspend fun auth(login: String = LoginData.login, password: String = LoginData.p * Save info about current [User] in memory */ private suspend fun getMyself(login: String) { - val r = client.get("$ruzapiURL/studentinfo") { + val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo") { parameter("uns", login.substring(1)) - } - val json = JSONObject(r.bodyAsText()) + }.bodyAsText()) + + val user = JSONObject( + client.get("$vuzapiURL/user") { + header("Cookie", "PHPSESSID=${PHPSESSID}") + }.bodyAsText() + ) + ME = User( - id = json.getString("id"), - uns = json.getString("uns"), - type = when(json.getString("type")) { + unnId = studentinfo.getString("id").toInt(), + bitrixId = user.getInt("bitrix_id"), + userId = user.getInt("id"), + type = when(studentinfo.getString("type")) { "lecturer" -> Type.Lecturer // ig,,, else -> Type.Student }, - email = json.getString("email"), - name = json.getString("fio"), - info = json.getString("info") + email = user.getString("email"), + nameRu = user.getString("fullname"), + 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 { +suspend fun getSchedule( + type: Type = ME.type, + id: Int = ME.unnId!!, + start: LocalDate, + finish: LocalDate +): ArrayList { val unnDatePattern = DateTimeFormatter.ofPattern("yyyy.MM.dd") 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( name = lecturer.getString("lecturer"), email = lecturer.getString("lecturerEmail"), - oid = lecturer.getInt("lecturerOid"), + unnId = lecturer.getInt("lecturerOid"), uid = lecturer.getString("lecturerUID"), rank = when (lecturer.getString("lecturer_rank")) { "АССИСТ" -> LecturerRank.Assistant @@ -190,3 +237,79 @@ suspend fun getSchedule(type: Type = ME.type, id: String = ME.id, start: LocalDa } 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 { + 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() + 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"), + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/sweetbread/unn/ui/composes/Blogpost.kt b/app/src/main/java/ru/sweetbread/unn/ui/composes/Blogpost.kt new file mode 100644 index 0000000..1c7f1b1 --- /dev/null +++ b/app/src/main/java/ru/sweetbread/unn/ui/composes/Blogpost.kt @@ -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 { + return getBlogposts() + } +} + +class PostViewModel : ViewModel() { + private val repository = PostRepository() + private val _posts = MutableStateFlow>(emptyList()) + val posts: StateFlow> = _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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt b/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt index ee28ef4..5a9b65b 100644 --- a/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt +++ b/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt @@ -7,12 +7,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -23,7 +20,6 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -45,7 +41,6 @@ import com.kizitonwose.calendar.compose.WeekCalendar import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.intellij.lang.annotations.JdkConstants.HorizontalAlignment import ru.sweetbread.unn.R import ru.sweetbread.unn.ui.Auditorium import ru.sweetbread.unn.ui.Building @@ -290,7 +285,7 @@ fun ScheduleItemPreview() { name = "Фамилия Имя Отчество", rank = LecturerRank.SLecturer, email = "", - oid = 28000, + unnId = 28000, uid = "51000" ) ), @@ -338,7 +333,7 @@ fun ScheduleExpandedItemPreview() { name = "Фамилия Имя Отчество", rank = LecturerRank.SLecturer, email = "", - oid = 28000, + unnId = 28000, uid = "51000" ) ), diff --git a/app/src/main/java/ru/sweetbread/unn/ui/layout/MainActivity.kt b/app/src/main/java/ru/sweetbread/unn/ui/layout/MainActivity.kt index ac3fdd3..0b3cc53 100644 --- a/app/src/main/java/ru/sweetbread/unn/ui/layout/MainActivity.kt +++ b/app/src/main/java/ru/sweetbread/unn/ui/layout/MainActivity.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.Logger 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.theme.UNNTheme import splitties.toast.toast +import java.io.File val client = HttpClient { install(HttpCache) @@ -59,34 +60,41 @@ val client = HttpClient { } } +val cacheDir = File("/data/data/ru.sweetbread.unn/files/cache") + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Log.d("mkdir", cacheDir.mkdir().toString()) setContent { UNNTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { val navController = rememberNavController() - var route by remember { mutableStateOf("home") } + var route by remember { mutableStateOf("portal/blogposts") } Scaffold( bottomBar = { NavigationBar { NavigationBarItem( - onClick = { toast("Not implemented") }, + onClick = { + route = "portal/blogposts" + navController.navigate(route) + }, icon = { Icon( Icons.Filled.Home, contentDescription = "Home" ) }, - selected = route.startsWith("home") + selected = route.startsWith("portal/") ) NavigationBarItem( onClick = { - navController.navigate("journal/schedule") - route = "journal/schedule" }, + route = "journal/schedule" + navController.navigate(route) + }, icon = { Icon( Icons.Filled.DateRange, @@ -110,9 +118,9 @@ class MainActivity : ComponentActivity() { } ) {innerPadding -> Box(Modifier.padding(innerPadding)) { - NavHost(navController, startDestination = "home/blogposts") { - composable("home/blogposts") { - Text("Not implemented") + NavHost(navController, startDestination = "portal/blogposts") { + composable("portal/blogposts") { + Blogposts() } composable("journal/schedule") { Schedule() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1d98a0..4bb9a4e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,5 +12,6 @@ Auditorium Building Floor + 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. \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f94753d..54a3695 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] acraHttp = "5.11.3" agp = "8.3.1" +coilCompose = "2.6.0" compose = "2.5.0" coreSplashscreen = "1.0.1" 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-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } 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" } 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" }