feat: Blogs

This commit is contained in:
sweetbread
2024-03-20 21:14:31 +03:00
parent 0d4ba49111
commit 3cf7885d9e
7 changed files with 378 additions and 36 deletions
+143 -20
View File
@@ -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<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 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<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"),
)
}
)
}
@@ -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
)
}
}
}
@@ -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"
)
),
@@ -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,6 +32,7 @@ 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
@@ -67,26 +67,30 @@ class MainActivity : ComponentActivity() {
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 +114,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()