Стабильная аунтификация

This commit is contained in:
2023-04-18 01:07:53 +03:00
parent 0ebc9f762b
commit 4029e93768
37 changed files with 528 additions and 97 deletions
+3 -3
View File
@@ -2,18 +2,18 @@ package ru.megboyzz.dnevnik
import android.app.Application import android.app.Application
import androidx.room.Room import androidx.room.Room
import ru.megboyzz.dnevnik.db.CredentialsDataBase import ru.megboyzz.dnevnik.db.AppDataBase
class App : Application() { class App : Application() {
lateinit var database: CredentialsDataBase lateinit var database: AppDataBase
private set private set
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
database = Room.databaseBuilder(this, CredentialsDataBase::class.java, "database") database = Room.databaseBuilder(this, AppDataBase::class.java, "database")
.build() .build()
} }
@@ -4,6 +4,7 @@ import android.app.Application
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -52,10 +53,12 @@ class MainActivity : ComponentActivity() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val credentialsDao = app.database.credentialsDao() val credentialsDao = app.database.credentialsDao()
val credentials = credentialsDao.getCredentials()
if(!credentials.rememberMe)
credentialsDao.deleteCredentials()
if(credentialsDao.credentialsIsExists()) {
val credentials = credentialsDao.getCredentials()
if (!credentials.rememberMe)
credentialsDao.deleteCredentials()
}
} }
} }
@@ -1,30 +1,54 @@
package ru.megboyzz.dnevnik.authorization package ru.megboyzz.dnevnik.authorization
import android.util.Log import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import ru.megboyzz.dnevnik.db.dao.CredentialsDao import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import ru.megboyzz.dnevnik.db.AppDataBase
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
import ru.megboyzz.dnevnik.entities.context.marks.init.state.SchoolType
import ru.megboyzz.dnevnik.entities.db.Credentials import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
import ru.megboyzz.dnevnik.service.ContextService
import java.net.URL import java.net.URL
class AuthorizationManager(private val credentialsDao: CredentialsDao) { class AuthorizationManager(dataBase: AppDataBase) {
private val returnUrl = private val returnUrl =
"https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo" "https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo"
private val baseUrl = "https://login.dnevnik.ru/login/" private val baseUrl = "https://login.dnevnik.ru/login/"
private val apiUrl = "https://api.dnevnik.ru"
private val retrofit = Retrofit
.Builder()
.baseUrl(apiUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val contextService = retrofit.create(ContextService::class.java)
private val marksState = "https://dnevnik.ru/marks"
private val credentialsDao = dataBase.credentialsDao()
private val userContextDao = dataBase.globalUserContextDao()
private var credentials = Credentials(token = "", cookies = mapOf(), rememberMe = false) private var credentials = Credentials(token = "", cookies = mapOf(), rememberMe = false)
private val gson = Gson()
fun login( fun login(
login: String, login: String,
password: String, password: String,
rememberMe: Boolean rememberMe: Boolean
): AuthorizationStatus { ): AuthorizationStatus {
val execute = runCatching<Connection.Response> { runCatching<Connection.Response> {
Jsoup Jsoup
.connect(baseUrl) .connect(baseUrl)
.method(Connection.Method.POST) .method(Connection.Method.POST)
@@ -34,26 +58,15 @@ class AuthorizationManager(private val credentialsDao: CredentialsDao) {
.followRedirects(true) .followRedirects(true)
.execute() .execute()
}.onFailure { }.onFailure {
Log.i("AuthorizationManager", "No internet")
return AuthorizationStatus.NO_INTERNET return AuthorizationStatus.NO_INTERNET
} }.onSuccess { response ->
if(response.statusCode() != 200)
val response = execute.getOrNull()
if(response != null){
if(response.statusCode() != 200) {
Log.i("AuthorizationManager", "Maintain")
return AuthorizationStatus.MAINTAIN return AuthorizationStatus.MAINTAIN
}
val url = response.url() val url = response.url()
if(url.getResult() != "success") { if(url.getResult() != "success")
Log.i("AuthorizationManager", "Wrong login")
return AuthorizationStatus.WRONG_CREDENTIALS return AuthorizationStatus.WRONG_CREDENTIALS
}
//Безопасная зона //Безопасная зона
val cookies = response.cookies() val cookies = response.cookies()
@@ -64,7 +77,39 @@ class AuthorizationManager(private val credentialsDao: CredentialsDao) {
) )
dbUpdate() dbUpdate()
}else return AuthorizationStatus.NO_INTERNET //Может быть опасно kotlin.runCatching {
Jsoup
.connect(marksState)
.method(Connection.Method.GET)
.cookies(cookies)
.ignoreContentType(true)
.followRedirects(true)
.execute()
}.onFailure {
return AuthorizationStatus.NO_INTERNET
}.onSuccess { response1 ->
val htmlByLines = response1.body().split("\n")
val contextLine = htmlByLines.find { it.contains("window.__MARKS__INITIAL__STATE__") }
val marksContextStr =
contextLine?.substring(contextLine.indexOf("{"), contextLine.length - 2)
val marksContext = gson.fromJson(marksContextStr, MarksContext::class.java)
val context = contextService.getContext(credentials.token, marksContext.context.userId).execute()
if(context.isSuccessful){
val body = context.body() ?: return AuthorizationStatus.UNKNOWN_ERROR
userContextDao.setGlobalUserContext(GlobalUserContext(
apiUserContext = body,
marksInitStateUserContext = marksContext
))
} else return AuthorizationStatus.UNKNOWN_ERROR
}
}
return AuthorizationStatus.LOGGED return AuthorizationStatus.LOGGED
@@ -7,6 +7,8 @@ enum class AuthorizationStatus {
WRONG_CREDENTIALS, WRONG_CREDENTIALS,
NO_INTERNET, NO_INTERNET,
MAINTAIN, MAINTAIN,
NOTHING CREDENTIALS_LOADING,
NOTHING,
UNKNOWN_ERROR
} }
@@ -0,0 +1,19 @@
package ru.megboyzz.dnevnik.db
import androidx.room.Database
import androidx.room.RoomDatabase
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
@Database(
entities = [Credentials::class, GlobalUserContext::class],
version = 1
)
abstract class AppDataBase: RoomDatabase() {
abstract fun credentialsDao(): CredentialsDao
abstract fun globalUserContextDao(): GlobalUserContextDao
}
@@ -1,16 +0,0 @@
package ru.megboyzz.dnevnik.db
import androidx.room.Database
import androidx.room.RoomDatabase
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.entities.db.Credentials
@Database(
entities = [Credentials::class],
version = 1
)
abstract class CredentialsDataBase: RoomDatabase() {
abstract fun credentialsDao(): CredentialsDao
}
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.converters
import androidx.room.TypeConverter
import com.google.gson.Gson
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
import ru.megboyzz.dnevnik.entities.response.UserContext
class DataStructureConverter {
val gson = Gson()
@TypeConverter
fun fromUserContext(userContext: UserContext): String = gson.toJson(userContext)
@TypeConverter
fun toUserContext(savedUserContext: String): UserContext = gson.fromJson(savedUserContext, UserContext::class.java)
@TypeConverter
fun fromMarksContext(marksContext: MarksContext): String = gson.toJson(marksContext)
@TypeConverter
fun toMarksContext(savedMarksContext: String): MarksContext = gson.fromJson(savedMarksContext, MarksContext::class.java)
}
@@ -10,7 +10,6 @@ import ru.megboyzz.dnevnik.entities.db.Credentials
@Dao @Dao
interface CredentialsDao { interface CredentialsDao {
@Query("SELECT EXISTS(SELECT * FROM Credentials WHERE id = 0)") @Query("SELECT EXISTS(SELECT * FROM Credentials WHERE id = 0)")
fun credentialsIsExists(): Boolean fun credentialsIsExists(): Boolean
@Query("SELECT * FROM Credentials WHERE id = 0") @Query("SELECT * FROM Credentials WHERE id = 0")
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
@Dao
interface GlobalUserContextDao {
@Query("SELECT EXISTS(SELECT * FROM GlobalUserContext WHERE id = 0)")
fun globalUserContextIsExists(): Boolean
@Query("SELECT * FROM GlobalUserContext WHERE id = 0")
fun getGlobalUserContext(): GlobalUserContext
@Query("DELETE FROM GlobalUserContext WHERE id = 0")
fun deleteGlobalUserContext()
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setGlobalUserContext(profile: GlobalUserContext)
}
@@ -0,0 +1,5 @@
package ru.megboyzz.dnevnik.entities.context
enum class GroupType {
Quarter
}
@@ -2,6 +2,6 @@ package ru.megboyzz.dnevnik.entities.context
data class PeriodGroup( data class PeriodGroup(
val id: Long, val id: Long,
val type: String, //Обычно равен "Quarter", то есть Четверть, но как будет семестр у них???? val type: GroupType, //Обычно равен "Quarter", то есть Четверть, но как будет семестр у них????
val periods: List<PeriodInfo> val periods: List<PeriodInfo>
) )
@@ -3,7 +3,7 @@ package ru.megboyzz.dnevnik.entities.context
data class PeriodInfo( data class PeriodInfo(
val id: Long, val id: Long,
val number: Short, val number: Short,
val type: String, val type: GroupType,
val dateStart: Long, val dateStart: Long,
val dateFinish: Long, val dateFinish: Long,
val studyYear: Short, val studyYear: Short,
@@ -8,7 +8,7 @@ data class PersonContext(
val middleName: String, val middleName: String,
val lastName: String, val lastName: String,
val avatarUrl: String, val avatarUrl: String,
val schoolInfo: SchoolInfo, val school: SchoolInfo,
val group: GroupInfo, val group: GroupInfo,
val reportingPeriodGroup: PeriodGroup val reportingPeriodGroup: PeriodGroup
) )
@@ -3,5 +3,6 @@ package ru.megboyzz.dnevnik.entities.context
data class SchoolInfo( data class SchoolInfo(
val id: Long, val id: Long,
val name: String, val name: String,
val avatarUrl: String val avatarUrl: String,
val type: SchoolType
) )
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.context
enum class SchoolType {
Additional,
Regular
}
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class Group(
val groupId: String,
val groupName: String,
val reportingPeriodNotSet: Boolean,
val periodTabName: String
)
@@ -0,0 +1,9 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class GroupHistorical(
val id: String,
val name: String,
val studyYear: Int,
val periods: List<Period>,
val periodTabName: String
)
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class MarksContext(
val apiUrl: String,
val context: UserContext
)
@@ -0,0 +1,9 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class Period(
val id: String,
val name: String,
val shortName: String,
val number: Int,
val isDefault: Boolean
)
@@ -0,0 +1,17 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class SchoolMembership(
val personId: String,
val firstName: String,
val lastName: String,
val avatarUrl: String,
val schoolId: String,
val schoolName: String,
val isOdo: Boolean,
val isTermReportEnabled: Boolean,
val studyYear: Int,
val schoolType: SchoolType,
val groups: List<Group>,
val holidays: List<String>,
val groupHistory: List<GroupHistorical>
)
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
enum class SchoolType {
Regular,
Additional
}
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.entities.context.marks.init.state
data class UserContext(
val userId: String,
val role: String,
val schoolMemberships: List<SchoolMembership>,
)
@@ -0,0 +1,17 @@
package ru.megboyzz.dnevnik.entities.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import ru.megboyzz.dnevnik.db.converters.DataStructureConverter
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
import ru.megboyzz.dnevnik.entities.response.UserContext
@Entity
@TypeConverters(DataStructureConverter::class)
data class GlobalUserContext(
@PrimaryKey
val id: Int = 0,
val apiUserContext: UserContext, // Обычный контекст из API
val marksInitStateUserContext: MarksContext //window.__MARKS__INITIAL__STATE__
)
@@ -3,6 +3,7 @@ package ru.megboyzz.dnevnik.entities.response
import ru.megboyzz.dnevnik.entities.context.PersonContext import ru.megboyzz.dnevnik.entities.context.PersonContext
import ru.megboyzz.dnevnik.entities.context.UserInfo import ru.megboyzz.dnevnik.entities.context.UserInfo
data class UserContext( data class UserContext(
val info: UserInfo, val info: UserInfo,
val contextPersons: List<PersonContext> val contextPersons: List<PersonContext>
@@ -1,5 +1,6 @@
package ru.megboyzz.dnevnik.navigation package ru.megboyzz.dnevnik.navigation
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.rememberScaffoldState import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -18,6 +19,7 @@ import ru.megboyzz.dnevnik.collectAsMutableState
import ru.megboyzz.dnevnik.screens.* import ru.megboyzz.dnevnik.screens.*
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModelFactory import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModelFactory
import ru.megboyzz.dnevnik.navigate
sealed class BaseNavRote(open val route: String) sealed class BaseNavRote(open val route: String)
@@ -52,11 +54,18 @@ fun AppNavHost(){
){ ){
composable(AppNavRoute.Splash.route) { composable(AppNavRoute.Splash.route) {
if(loginStatus.value != AuthorizationStatus.LOGGED){ when(loginStatus.value){
navController.navigate(AppNavRoute.Login.route)
}else
navController.navigate(AppNavRoute.Marks.route)
AuthorizationStatus.CREDENTIALS_LOADING -> { CircularProgressIndicator() }
AuthorizationStatus.LOGGED -> {
navController.navigate(AppNavRoute.Marks)
}
AuthorizationStatus.NOTHING -> {
navController.navigate(AppNavRoute.Login)
}
else -> {}
}
} }
composable(AppNavRoute.Login.route) { LoginScreen(navController) } composable(AppNavRoute.Login.route) { LoginScreen(navController) }
@@ -7,6 +7,7 @@ import androidx.navigation.NavController
import ru.megboyzz.dnevnik.MainActivity import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import ru.megboyzz.dnevnik.collectAsMutableState import ru.megboyzz.dnevnik.collectAsMutableState
import ru.megboyzz.dnevnik.navigate
import ru.megboyzz.dnevnik.navigation.AppNavRoute import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.ui.AlertMessageBox import ru.megboyzz.dnevnik.screens.ui.AlertMessageBox
import ru.megboyzz.dnevnik.screens.ui.LoginScreenContent import ru.megboyzz.dnevnik.screens.ui.LoginScreenContent
@@ -35,6 +36,12 @@ fun LoginScreen(navController: NavController) {
mutableStateOf(true) mutableStateOf(true)
} }
var isLoading by remember {
mutableStateOf(false)
}
if(loginStatus.value == AuthorizationStatus.LOGGING) isLoading = true
if(loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS && isError) { if(loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS && isError) {
AlertMessageBox( AlertMessageBox(
title = "Информация", title = "Информация",
@@ -44,7 +51,7 @@ fun LoginScreen(navController: NavController) {
} }
if(loginStatus.value == AuthorizationStatus.LOGGED) if(loginStatus.value == AuthorizationStatus.LOGGED)
navController.navigate(AppNavRoute.Marks.route) navController.navigate(AppNavRoute.Marks)
LoginScreenContent( LoginScreenContent(
login = login.value, login = login.value,
@@ -56,7 +63,7 @@ fun LoginScreen(navController: NavController) {
isError = loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS, isError = loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS,
onErrorChange = { }, onErrorChange = { },
isLoading = loginStatus.value == AuthorizationStatus.LOGGING, isLoading = isLoading,
isRememberMe = rememberMe.value, isRememberMe = rememberMe.value,
onRememberChange = { rememberMe.value = it }, onRememberChange = { rememberMe.value = it },
@@ -1,5 +1,6 @@
package ru.megboyzz.dnevnik.screens.ui package ru.megboyzz.dnevnik.screens.ui
import android.app.AlertDialog
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -8,6 +9,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
@@ -20,11 +22,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -33,9 +37,11 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import coil.compose.AsyncImage
import ru.megboyzz.dnevnik.* import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.* import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
//Рефакторинг компонентов: разбиение файла с компонентами на отдельные файлы //Рефакторинг компонентов: разбиение файла с компонентами на отдельные файлы
@@ -73,25 +79,29 @@ fun AlmostOutlinedText(
@Composable @Composable
fun ProfileCard( fun ProfileCard(
textColor: Color = white, textColor: Color = white,
painter: Painter userProfileModel: UserProfileModel
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
Image( AsyncImage(
modifier = Modifier.padding(10.dp, 15.dp, 20.dp, 15.dp), modifier = Modifier
painter = painter, //TODO заменить динмической картинкой .padding(10.dp, 15.dp, 20.dp, 15.dp)
contentDescription = "chel" .clip(RoundedCornerShape(25.dp))
.size(50.dp),
contentScale = ContentScale.Crop,
model = userProfileModel.avatarUrl,
contentDescription = ""
) )
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text( Text(
text = "Имя и фамилия", text = userProfileModel.toString(),
style = H1, style = H1,
color = textColor color = textColor
) )
Text( Text(
text = "Класс/Группа", text = userProfileModel.groupName,
style = H1, style = H1,
color = textColor color = textColor
) )
@@ -640,3 +650,65 @@ fun AlertMessageBox(
) )
} }
@Composable
fun LeaveAlert(
isLoading: Boolean = false,
onAgree: () -> Unit,
onCancel: () -> Unit,
){
BaseAlertBox(
title = R.string.tile_leave.AsString(),
onDismissRequest = onCancel,
content = {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
text = R.string.text_leave.AsString(),
style = H2,
color = dark
)
if(isLoading) CircularProgressIndicator()
}
},
buttons = {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
AlertButton(
title = R.string.title_cancel.AsString(),
onClick = onCancel
)
SpacerWidth(width = 10.dp)
AlertButton(
title = R.string.title_button_leave.AsString(),
onClick = onAgree
)
}
}
)
}
@Composable
fun AlertButton(
title: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = mainBlue,
contentColor = white
),
modifier = Modifier
.padding(bottom = 30.dp)
){
Text(
text = title,
style = H1
)
}
}
@@ -392,4 +392,13 @@ fun Test1() {
onClick = { /*TODO*/ } onClick = { /*TODO*/ }
) )
}
@Preview
@Composable
fun AlertTest() {
LeaveAlert(onAgree = { /*TODO*/ }) {
}
} }
@@ -5,12 +5,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider import androidx.compose.material.*
import androidx.compose.material.ScaffoldState import androidx.compose.runtime.*
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -20,24 +16,34 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.* import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.navigation.AppNavRoute import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.ui.AlmostOutlinedText import ru.megboyzz.dnevnik.screens.ui.AlmostOutlinedText
import ru.megboyzz.dnevnik.screens.ui.LeaveAlert
import ru.megboyzz.dnevnik.screens.ui.ProfileCard import ru.megboyzz.dnevnik.screens.ui.ProfileCard
import ru.megboyzz.dnevnik.ui.theme.H2 import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.Shapes import ru.megboyzz.dnevnik.ui.theme.Shapes
import ru.megboyzz.dnevnik.ui.theme.mainBlue import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModel
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModelFactory
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
@Composable @Composable
@@ -77,7 +83,43 @@ fun DrawerContent(
scaffoldState: ScaffoldState scaffoldState: ScaffoldState
) { ) {
val app = (LocalContext.current as MainActivity).app
val profileViewModel: ProfileViewModel = viewModel(
factory = ProfileViewModelFactory(app)
)
val profile = profileViewModel.profile.collectAsState()
val dao = app.database.credentialsDao()
var leaveAlertIsOpened by remember {
mutableStateOf(false)
}
var isLeavingProcess by remember {
mutableStateOf(false)
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if(leaveAlertIsOpened){
LeaveAlert(
isLoading = isLeavingProcess,
onAgree = {
scope.launch(Dispatchers.IO) {
isLeavingProcess = true
dao.deleteCredentials()
leaveAlertIsOpened = false
this.launch(Dispatchers.Main) { navController.navigate(AppNavRoute.Login) }
scaffoldState.drawerState.close()
}
},
onCancel = { leaveAlertIsOpened = false }
)
}
NiceContainerForDrawer { NiceContainerForDrawer {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -88,9 +130,20 @@ fun DrawerContent(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
) { ) {
SpacerHeight(10.dp) SpacerHeight(10.dp)
ProfileCard(painter = R.drawable.ic_author.AsPainter()) if(profile.value == null)
CircularProgressIndicator()
else
ProfileCard(userProfileModel = profile.value!!)
SpacerHeight(15.dp) SpacerHeight(15.dp)
AlmostOutlinedText(text = "Идет 3-я четверть") if(profile.value == null)
CircularProgressIndicator()
else
AlmostOutlinedText(
text = if(profile.value!!.isTerm)
String.format(R.string.title_quatter_format.AsString(), profile.value!!.term)
else
String.format(R.string.title_sem_format.AsString(), profile.value!!.term)
)
SpacerHeight(20.dp) SpacerHeight(20.dp)
Column( Column(
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
@@ -135,7 +188,9 @@ fun DrawerContent(
DrawerMainButton( DrawerMainButton(
icon = R.drawable.ic_exit.AsPainter(), icon = R.drawable.ic_exit.AsPainter(),
text = R.string.title_leave_from_acconut.AsString() text = R.string.title_leave_from_acconut.AsString()
) { /* TODO */ } ) {
leaveAlertIsOpened = true
}
} }
} }
} }
@@ -1,10 +1,7 @@
package ru.megboyzz.dnevnik.screens.ui.main package ru.megboyzz.dnevnik.screens.ui.main
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
@@ -25,6 +22,7 @@ import ru.megboyzz.dnevnik.SpacerWidth
import ru.megboyzz.dnevnik.ui.theme.mainBlue import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.R import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
@Composable @Composable
fun MainScaffold( fun MainScaffold(
@@ -32,7 +30,7 @@ fun MainScaffold(
title: String, title: String,
navController: NavController, navController: NavController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
content: (@Composable (it: PaddingValues) -> Unit), content: @Composable (PaddingValues) -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -52,6 +50,9 @@ fun MainScaffold(
navController = navController, navController = navController,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
bottomBar = { BottomBar(navController) }, bottomBar = { BottomBar(navController) },
drawerContent = {
DrawerContent(navController, scaffoldState)
},
content = content content = content
) )
} }
@@ -65,6 +66,7 @@ fun BaseScaffold(
navController: NavController, navController: NavController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
bottomBar: @Composable () -> Unit, bottomBar: @Composable () -> Unit,
drawerContent: @Composable (ColumnScope) -> Unit = {},
content: (@Composable (it: PaddingValues) -> Unit), content: (@Composable (it: PaddingValues) -> Unit),
) { ) {
Scaffold( Scaffold(
@@ -100,9 +102,7 @@ fun BaseScaffold(
drawerContentColor = Color.Transparent, drawerContentColor = Color.Transparent,
//drawerShape = drawerShape, //drawerShape = drawerShape,
drawerElevation = 0.dp, drawerElevation = 0.dp,
drawerContent = { drawerContent = drawerContent,
DrawerContent(navController, scaffoldState)
},
bottomBar = bottomBar bottomBar = bottomBar
) )
} }
@@ -0,0 +1,12 @@
package ru.megboyzz.dnevnik.service
import retrofit2.Call
import retrofit2.http.*
import ru.megboyzz.dnevnik.entities.response.UserContext
interface ContextService{
@GET("/mobile/v4/users/{personId}/context")
fun getContext(@Header("Access-Token") accessToken: String, @Path("personId") personId: String): Call<UserContext>
}
@@ -1,18 +0,0 @@
package ru.megboyzz.dnevnik.service
import okhttp3.ResponseBody
import org.jsoup.Jsoup
import retrofit2.http.POST
import retrofit2.http.Query
interface TokenService{
@POST("/")
suspend fun getToken(
@Query("login") login: String,
@Query("password") password: String,
@Query("ReturnUrl") returnUrl: String = "https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo"
): retrofit2.Call<ResponseBody>
}
@@ -19,8 +19,8 @@ class AuthorizationViewModel(application: App) : AndroidViewModel(application) {
val password = MutableStateFlow("") val password = MutableStateFlow("")
val rememberMe = MutableStateFlow(false) val rememberMe = MutableStateFlow(false)
val loginStatus = MutableStateFlow(AuthorizationStatus.NOTHING) val loginStatus = MutableStateFlow(AuthorizationStatus.CREDENTIALS_LOADING)
private val authManager = AuthorizationManager(application.database.credentialsDao()) private val authManager = AuthorizationManager(application.database)
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -0,0 +1,65 @@
package ru.megboyzz.dnevnik.viewmodel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.App
import ru.megboyzz.dnevnik.entities.context.GroupType
import ru.megboyzz.dnevnik.entities.context.SchoolType
import ru.megboyzz.dnevnik.entities.db.GlobalUserContext
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
import java.lang.IllegalArgumentException
class ProfileViewModel(application: App) : AndroidViewModel(application) {
val profile = MutableStateFlow<UserProfileModel?>(null)
val userContextDao = application.database.globalUserContextDao()
init{
viewModelScope.launch(Dispatchers.IO) {
val globalUserContext = userContextDao.getGlobalUserContext()
val convertedProfile = convertGlobalUserContextToProfile(globalUserContext)
profile.emit(convertedProfile)
}
}
private fun convertGlobalUserContextToProfile(
globalUserContext: GlobalUserContext
): UserProfileModel{
val info = globalUserContext.apiUserContext.info
val personContext = globalUserContext.apiUserContext.contextPersons.find {
it.school.type == SchoolType.Regular
} ?: globalUserContext.apiUserContext.contextPersons[0]
val term = personContext.reportingPeriodGroup.periods.find {
it.isCurrent
} ?: personContext.reportingPeriodGroup.periods[0]
return UserProfileModel(
firstName = info.firstName,
middleName = info.middleName,
lastName = info.lastName,
groupName = personContext.group.name,
term = term.number + 1,
isTerm = term.type == GroupType.Quarter,
avatarUrl = info.avatarUrl
)
}
}
class ProfileViewModelFactory(private val application: App) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProfileViewModel::class.java))
return ProfileViewModel(application) as T
throw IllegalArgumentException("Unknown ViewModel Class")
}
}
@@ -0,0 +1,13 @@
package ru.megboyzz.dnevnik.viewmodel.model
data class UserProfileModel(
val firstName: String,
val middleName: String,
val lastName: String,
val groupName: String,
val term: Int,
val isTerm: Boolean,
val avatarUrl: String
){
override fun toString() = "$lastName ${firstName.first()}. ${middleName.first()}."
}
@@ -72,4 +72,10 @@
<string name="title_choose_icons">" Select the icons for subjects"</string> <string name="title_choose_icons">" Select the icons for subjects"</string>
<string name="title_enable_notif">Enable notify</string> <string name="title_enable_notif">Enable notify</string>
<string name="title_ok">ОК</string> <string name="title_ok">ОК</string>
<string name="tile_leave">Leave</string>
<string name="text_leave">Are you sure you want to exit the diary?</string>
<string name="title_button_leave">Leave</string>
<string name="title_cancel">Cancel</string>
<string name="title_quatter_format">Quatter %d</string>
<string name="title_sem_format">Semester %d</string>
</resources> </resources>
+6
View File
@@ -71,4 +71,10 @@
<string name="title_choose_icons">Выбрать иконки для предметов</string> <string name="title_choose_icons">Выбрать иконки для предметов</string>
<string name="title_enable_notif">Включить уведомления о новых оценках</string> <string name="title_enable_notif">Включить уведомления о новых оценках</string>
<string name="title_ok">Ясно</string> <string name="title_ok">Ясно</string>
<string name="tile_leave">Выход</string>
<string name="text_leave">Вы точно хотите выйти из дневника? Авторизационные данные будут удалены.</string>
<string name="title_button_leave">Выйти</string>
<string name="title_cancel">Отмена</string>
<string name="title_quatter_format">Идет %d-я четверть</string>
<string name="title_sem_format">Идет %d-ий семестр</string>
</resources> </resources>