Compare commits

...
10 Commits
141 changed files with 5729 additions and 526 deletions
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-123
View File
@@ -1,123 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="localhost:58526" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-04-02T15:04:59.965857200Z" />
</component>
</project>
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="corretto-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-23
View File
@@ -1,23 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17_PREVIEW" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+35
View File
@@ -0,0 +1,35 @@
package com.example.myapplication;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View v){
Intent intent = new Intent(this, PuppetActivity.class);
ResultHandler.addResultHandler(intent, () -> {
Toast.makeText(this, "lol!", Toast.LENGTH_LONG).show();
});
startActivityForResult(intent, 0);
//ResultHandler handler = new ResultHandler(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
ResultHandler handler = new ResultHandler(this);
handler.onIncomingIntent(requestCode, resultCode, data);
}
}
+14
View File
@@ -0,0 +1,14 @@
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class PuppetActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pupet);
}
}
+5
View File
@@ -0,0 +1,5 @@
package com.example.myapplication;
public interface ResultAction {
void action();
}
+56
View File
@@ -0,0 +1,56 @@
package com.example.myapplication;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class ResultHandler {
private Context context;
private static Map<Integer, ResultAction> map = new HashMap<>();
private static final int RANDOM_BOUNDS = 256;
private static final String LOG_TAG = "ResultHandler";
public static final String ACTION_ID_TITLE = "ACTION_ID";
public ResultHandler(Context context) {
this.context = context;
}
/**
* Статические поля кодов обработки входящих интентов
*/
public enum Fields{
FILE_OPEN,
}
public static void addResultHandler(Intent intent, ResultAction action){
int random = (new Random()).nextInt(RANDOM_BOUNDS);
intent.putExtra(ACTION_ID_TITLE, random);
map.put(random, action);
}
public void onIncomingIntent(int requestCode, int resultCode, Intent data){
if(data != null){
if(data.hasExtra(ACTION_ID_TITLE)){
int id = data.getExtras().getInt(ACTION_ID_TITLE);
map.get(id).action();
}else{
Log.e(LOG_TAG, "Incoming intent haven't got ACTION_ID!!");
throw new RuntimeException(
"ResultHandler::RuntimeException can't parse incoming intent! Shutting down(("
);
}
}else{
Log.e(LOG_TAG, "Incoming intent is null!!");
throw new RuntimeException(
"ResultHandler::RuntimeException can't parse incoming intent! Shutting down(("
);
}
}
}
+29 -6
View File
@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
}
android {
@@ -31,11 +32,11 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
freeCompilerArgs = listOf(
"-Xjvm-default=compatibility"
)
@@ -56,13 +57,35 @@ android {
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
tasks.register("connectToWSA")
tasks {
named("connectToWSA") {
exec {
commandLine = "adb connect localhost:58526".split(" ")
}
}
}
dependencies {
//std lib
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
//app libs
implementation(AppDependencies.appLibraries)
implementation(AppLibraries)
//test libs
testImplementation(AppDependencies.testLibraries)
androidTestImplementation(AppDependencies.androidTestLibraries)
testImplementation(TestLibraries)
androidTestImplementation(AndroidTestLibraries)
kapt(KaptLibraries)
coreLibraryDesugaring(DesugaringLibraries)
debugImplementation(DebugLibraries)
}
+1
View File
@@ -13,6 +13,7 @@
android:roundIcon="@drawable/icon_rounded"
android:supportsRtl="true"
android:theme="@style/Theme.MyDnevnik"
android:name=".App"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@@ -0,0 +1,31 @@
package ru.megboyzz.dnevnik
import android.app.Application
import android.content.Context
import androidx.room.Room
import ru.megboyzz.dnevnik.db.AppDataBase
import ru.megboyzz.dnevnik.di.AppComponent
import ru.megboyzz.dnevnik.di.DaggerAppComponent
import ru.megboyzz.dnevnik.di.modules.AppModule
class App : Application() {
lateinit var appComponent: AppComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent
.builder()
.appModule(AppModule(this))
.build()
}
}
/*
* Специальное расширение для получения AppComponent в любом месте, где у вас есть доступ к Context.
* Позволяет избегать статического хранения ссылки на ваш [Application] класс
*/
val Context.appComponent: AppComponent
get() = when (this) {
is App -> appComponent
else -> applicationContext.appComponent
}
@@ -1,21 +1,39 @@
package ru.megboyzz.dnevnik
import android.content.Context
import android.os.Bundle
import android.window.SplashScreen
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.navigation.AppNavHost
import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.ui.theme.lightGray
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
import javax.inject.Inject
import kotlin.concurrent.thread
class MainActivity : ComponentActivity() {
lateinit var app: App
@Inject
lateinit var credentialsDao: CredentialsDao
@Inject
lateinit var globalUserContext: GlobalUserContextDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.app = application as App
app.appComponent.inject(this)
setContent {
Surface(
modifier = Modifier.fillMaxSize(),
color = lightGray
@@ -24,4 +42,19 @@ class MainActivity : ComponentActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
thread {
if(credentialsDao.credentialsIsExists()) {
val credentials = credentialsDao.getCredentials()
if (!credentials.rememberMe) {
credentialsDao.deleteCredentials()
globalUserContext.deleteGlobalUserContext()
}
}
}
}
}
@@ -0,0 +1,153 @@
package ru.megboyzz.dnevnik.authorization
import android.util.Log
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Connection
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.di.DaggerAppComponent
import ru.megboyzz.dnevnik.entities.context.marks.init.state.MarksContext
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 javax.inject.Inject
class AuthorizationManager @Inject constructor(
val contextService: ContextService,
val credentialsDao: CredentialsDao,
val globalUserContextDao: GlobalUserContextDao
) {
private val returnUrl =
"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 marksState = "https://dnevnik.ru/marks"
private var credentials = Credentials(token = "", cookies = mapOf(), rememberMe = false)
private val gson = Gson()
fun login(
login: String,
password: String,
rememberMe: Boolean
): AuthorizationStatus {
runCatching<Connection.Response> {
Jsoup
.connect(baseUrl)
.method(Connection.Method.POST)
.data("ReturnUrl", returnUrl)
.data("login", login)
.data("password", password)
.followRedirects(true)
.execute()
}.onFailure {
if(it is HttpStatusException && it.statusCode == 405)
return AuthorizationStatus.MAINTAIN
return AuthorizationStatus.NO_INTERNET
}.onSuccess { response ->
if(response.statusCode() != 200)
return AuthorizationStatus.MAINTAIN
val url = response.url()
if(url.getResult() != "success")
return AuthorizationStatus.WRONG_CREDENTIALS
//Безопасная зона
val cookies = response.cookies()
credentials = Credentials(
token = url.getToken(),
cookies = cookies,
rememberMe = rememberMe
)
dbUpdate()
kotlin.runCatching {
Jsoup
.connect(marksState)
.method(Connection.Method.GET)
.cookies(cookies)
.ignoreContentType(true)
.followRedirects(true)
.execute()
}.onFailure {
Log.e("Auth", it.message.toString())
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
globalUserContextDao.setGlobalUserContext(GlobalUserContext(
apiUserContext = body,
marksInitStateUserContext = marksContext
))
} else return AuthorizationStatus.UNKNOWN_ERROR
}
}
return AuthorizationStatus.LOGGED
}
fun checkCredentials(): AuthorizationStatus{
val credentialsIsExists = credentialsDao.credentialsIsExists()
return if(credentialsIsExists)
AuthorizationStatus.LOGGED
else
AuthorizationStatus.NOTHING
}
fun dbUpdate(){
credentialsDao.setCredentials(credentials)
}
fun deleteCredentials(){
credentialsDao.deleteCredentials()
}
fun URL.getResult(): String{
val httpUrl = this.toString().toHttpUrlOrNull() ?: return ""
return httpUrl.queryParameter("result") ?: ""
}
fun URL.getToken(): String{
val tokenRegex = "access_token=([A-Za-z0-9]+)"
val regex = Regex(tokenRegex)
val find = regex.find(this.toString())
if(find != null)
return find.groupValues[1]
return ""
}
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.authorization
enum class AuthorizationStatus {
LOGGED,
LOGGING,
WRONG_CREDENTIALS,
NO_INTERNET,
MAINTAIN,
CREDENTIALS_LOADING,
NOTHING,
UNKNOWN_ERROR,
LOGOUT
}
@@ -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
}
@@ -0,0 +1,18 @@
package ru.megboyzz.dnevnik.db.converters
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class CookiesConverter {
private val gson = Gson()
private val mapType = object : TypeToken<Map<String, String>>() {}.type
@TypeConverter
fun fromMap(cookies: Map<String, String>): String = gson.toJson(cookies)
@TypeConverter
fun toMap(savedCookies: String): Map<String, String> = gson.fromJson(savedCookies, mapType)
}
@@ -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)
}
@@ -0,0 +1,24 @@
package ru.megboyzz.dnevnik.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.megboyzz.dnevnik.entities.db.Credentials
@Dao
interface CredentialsDao {
@Query("SELECT EXISTS(SELECT * FROM Credentials WHERE id = 0)")
fun credentialsIsExists(): Boolean
@Query("SELECT * FROM Credentials WHERE id = 0")
fun getCredentials(): Credentials
@Query("DELETE FROM Credentials WHERE id = 0")
fun deleteCredentials()
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setCredentials(profile: Credentials)
}
@@ -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,42 @@
package ru.megboyzz.dnevnik.di
import android.app.Activity
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import dagger.BindsInstance
import dagger.Component
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.di.modules.AppModule
import ru.megboyzz.dnevnik.entities.db.Credentials
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModel
import ru.megboyzz.dnevnik.viewmodel.subscreen.LastMarksViewModel
import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
val authViewModel: AuthorizationViewModel
val profileViewModel: ProfileViewModel
val lastMarksViewModel: LastMarksViewModel
val credentialsDao: CredentialsDao
val globalUserContextDao: GlobalUserContextDao
fun inject(viewModel: ProfileViewModel)
fun inject(activity: MainActivity)
@Component.Builder
interface Builder {
fun appModule(appModule: AppModule): Builder // add this
fun build(): AppComponent
}
}
@@ -0,0 +1,42 @@
package ru.megboyzz.dnevnik.di.modules
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import ru.megboyzz.dnevnik.service.ContextService
import ru.megboyzz.dnevnik.service.MarksService
import ru.megboyzz.dnevnik.util.gson.DateDeserializer
import ru.megboyzz.dnevnik.util.gson.DateSerializer
import java.util.*
@Module
class ApiModule {
private val apiUrl = "https://api.dnevnik.ru"
@Provides
fun provideApiRetrofit(gson: Gson): Retrofit{
return Retrofit
.Builder()
.baseUrl(apiUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
@Provides
fun provideGson(): Gson = GsonBuilder()
//.registerTypeAdapter(Date::class.java, DateSerializer())
.registerTypeAdapter(Date::class.java, DateDeserializer())
.create()
@Provides
fun provideContextService(retrofit: Retrofit): ContextService = retrofit.create()
@Provides
fun provideMarksService(retrofit: Retrofit): MarksService = retrofit.create()
}
@@ -0,0 +1,20 @@
package ru.megboyzz.dnevnik.di.modules
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(includes = [ApiModule::class, DatabaseModule::class, AuthModule::class])
class AppModule(private val application: Application) {
@Provides
@Singleton
fun provideContext(): Context = this.application.applicationContext
@Provides
@Singleton
fun provideApplication(): Application = application
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.di.modules
import dagger.Module
import dagger.Provides
import ru.megboyzz.dnevnik.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.service.ContextService
@Module
class AuthModule {
}
@@ -0,0 +1,33 @@
package ru.megboyzz.dnevnik.di.modules
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.coroutineScope
import ru.megboyzz.dnevnik.db.AppDataBase
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.db.dao.GlobalUserContextDao
import ru.megboyzz.dnevnik.entities.db.Credentials
import javax.inject.Singleton
import kotlin.coroutines.coroutineContext
@Module
class DatabaseModule {
@Singleton
@Provides
fun provideDatabase(context: Context): AppDataBase {
return Room
.databaseBuilder(context, AppDataBase::class.java, "database")
.build()
}
@Provides
fun provideCredentialsDao(dataBase: AppDataBase): CredentialsDao = dataBase.credentialsDao()
@Provides
fun provideGlobalUserContextDao(dataBase: AppDataBase): GlobalUserContextDao = dataBase.globalUserContextDao()
}
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.context
data class GroupInfo(
val id: Long,
val name: String
)
@@ -0,0 +1,7 @@
package ru.megboyzz.dnevnik.entities.context
data class PeriodGroup(
val id: Long,
val type: PeriodType, //Обычно равен "Quarter", то есть Четверть, но как будет семестр у них????
val periods: List<PeriodInfo>
)
@@ -0,0 +1,11 @@
package ru.megboyzz.dnevnik.entities.context
data class PeriodInfo(
val id: Long,
val number: Short,
val type: PeriodType,
val dateStart: Long,
val dateFinish: Long,
val studyYear: Short,
val isCurrent: Boolean
)
@@ -0,0 +1,5 @@
package ru.megboyzz.dnevnik.entities.context
enum class PeriodType {
Quarter
}
@@ -0,0 +1,14 @@
package ru.megboyzz.dnevnik.entities.context
data class PersonContext(
val sex: String,
val userId: Long,
val personId: Long,
val firstName: String,
val middleName: String,
val lastName: String,
val avatarUrl: String,
val school: SchoolInfo,
val group: GroupInfo,
val reportingPeriodGroup: PeriodGroup
)
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.entities.context
data class SchoolInfo(
val id: Long,
val name: 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,12 @@
package ru.megboyzz.dnevnik.entities.context
data class UserInfo(
val sex: String,
val roles: List<String>,
val userId: Long,
val personId: Long,
val firstName: String,
val middleName: String,
val lastName: String,
val avatarUrl: String,
)
@@ -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,16 @@
package ru.megboyzz.dnevnik.entities.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import ru.megboyzz.dnevnik.db.converters.CookiesConverter
@Entity
@TypeConverters(CookiesConverter::class)
data class Credentials(
@PrimaryKey
val id: Int = 0,
val token: String,
val cookies: Map<String, String>,
val rememberMe: Boolean
)
@@ -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__
)
@@ -0,0 +1,18 @@
package ru.megboyzz.dnevnik.entities.lessons
import com.google.gson.annotations.SerializedName
import java.util.*
data class Lesson(
val id: Long,
val title: String,
val date: Date,
val number: Int,
val subjectId: Long,
val place: String,
val hours: String,
@SerializedName("works")
val workIds: List<Long>,
@SerializedName("teachers")
val teachersId: List<Long>
)
@@ -0,0 +1,18 @@
package ru.megboyzz.dnevnik.entities.marks
import com.google.gson.annotations.SerializedName
import java.util.*
data class Mark(
val id: Long,
val value: String,
@SerializedName("person")
val personId: Long,
@SerializedName("work")
val workId: Long,
@SerializedName("lesson")
val lessonId: Long,
val number: Int,
val date: Date,
val workType: Long
)
@@ -0,0 +1,14 @@
package ru.megboyzz.dnevnik.entities.marks
import ru.megboyzz.dnevnik.entities.lessons.Lesson
import ru.megboyzz.dnevnik.entities.subjects.Subject
import ru.megboyzz.dnevnik.entities.works.Work
import ru.megboyzz.dnevnik.entities.works.WorkType
data class RecentMarks(
val marks: List<Mark>,
val works: List<Work>,
val subjects: List<Subject>,
val workTypes: List<WorkType>,
val lessons: List<Lesson>,
)
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.response
data class TokenResponse(
val login: String,
val password: String
)
@@ -0,0 +1,10 @@
package ru.megboyzz.dnevnik.entities.response
import ru.megboyzz.dnevnik.entities.context.PersonContext
import ru.megboyzz.dnevnik.entities.context.UserInfo
data class UserContext(
val info: UserInfo,
val contextPersons: List<PersonContext>
)
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.subjects
data class Subject(
val id: Long,
val name: String
)
@@ -0,0 +1,12 @@
package ru.megboyzz.dnevnik.entities.teachers
data class Teacher(
val id: Long,
val userId: Long,
val firstName: String,
val lastName: String,
val middleName: String,
val sex: TeacherSex,
){
override fun toString() = "$lastName ${firstName.first()}. ${middleName.first()}."
}
@@ -0,0 +1,6 @@
package ru.megboyzz.dnevnik.entities.teachers
enum class TeacherSex {
Male,
Female
}
@@ -0,0 +1,23 @@
package ru.megboyzz.dnevnik.entities.works
import com.google.gson.annotations.SerializedName
import java.util.*
data class Work(
val id: Long,
val workType: Long,
val markCount: Int,
@SerializedName("lesson")
val lessonId: Long,
val displayInJournal: Boolean,
@SerializedName("eduGroup")
val eduGroupId: Long,
val text: String,
val periodNumber: Int,
val periodType: String,
val subjectId: Long,
val isImportant: Boolean,
val targetDate: Date,
val sentDate: Date?,
val files: List<Int>?
)
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.entities.works
data class WorkType(
val id: Long,
val schoolId: Long,
val abbreviation: String,
val name: String
)
@@ -1,22 +1,22 @@
package ru.megboyzz.dnevnik.navigation
import android.window.SplashScreen
import androidx.compose.animation.*
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import android.util.Log
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import ru.megboyzz.dnevnik.screens.*
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import androidx.activity.viewModels
import ru.megboyzz.dnevnik.appComponent
import ru.megboyzz.dnevnik.util.collectAsMutableState
import ru.megboyzz.dnevnik.util.navigate
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
sealed class BaseNavRote(open val route: String)
@@ -31,18 +31,45 @@ sealed class AppNavRoute(override val route: String): BaseNavRote(route){
object Settings: AppNavRoute("settings_screen")
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppNavHost(){
Log.i("AppNavHost", "Called")
val scaffoldState = rememberScaffoldState()
val navController = rememberAnimatedNavController()
val navController = rememberNavController()
val mainActivity = LocalContext.current as MainActivity
val authorizationViewModel: AuthorizationViewModel by mainActivity.viewModels{
ViewModelFactory{
Log.i("AuthorizationViewModel", "i am created!")
mainActivity.appComponent.authViewModel
}
}
val loginStatus = authorizationViewModel.loginStatus.collectAsMutableState()
NavHost(
navController = navController,
startDestination = AppNavRoute.Login.route /* сделать проверку куки файлов */
startDestination = AppNavRoute.Splash.route
){
composable(AppNavRoute.Splash.route) { }
composable(AppNavRoute.Splash.route) {
when(loginStatus.value){
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.Marks.route) { MarksScreen(navController, scaffoldState) }
composable(AppNavRoute.Schedule.route) { ScheduleScreen(navController, scaffoldState) }
@@ -7,17 +7,13 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavigatorProvider
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.createGraph
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import ru.megboyzz.dnevnik.AsString
import androidx.navigation.compose.rememberNavController
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.*
import ru.megboyzz.dnevnik.screens.subscreens.LastMarksSubScreen
import ru.megboyzz.dnevnik.screens.ui.SubScreenData
import ru.megboyzz.dnevnik.screens.ui.SubScreenNavBar
@@ -31,11 +27,10 @@ sealed class MarksNavRoute(override val route: String): BaseNavRote(route){
object ByYearMarks: MarksNavRoute("by_terms_marks")
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MarksSubScreenNavHost() {
val navController = rememberAnimatedNavController()
val navController = rememberNavController()
val list = listOf(
SubScreenData(
@@ -82,7 +77,7 @@ fun MarksSubScreenNavHost() {
navController = navController,
startDestination = MarksNavRoute.LastMarks.route
) {
composable(MarksNavRoute.LastMarks.route) { Text(MarksNavRoute.LastMarks.route) }
composable(MarksNavRoute.LastMarks.route) { LastMarksSubScreen() }
composable(MarksNavRoute.ByDaysMarks.route) { Text(MarksNavRoute.ByDaysMarks.route) }
composable(MarksNavRoute.StatsByMarks.route) { Text(MarksNavRoute.StatsByMarks.route) }
composable(MarksNavRoute.StatsBySubjects.route) { Text(MarksNavRoute.StatsBySubjects.route) }
@@ -3,8 +3,8 @@ package ru.megboyzz.dnevnik.screens
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.ui.main.MainScaffold
@@ -1,36 +1,145 @@
package ru.megboyzz.dnevnik.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import android.util.Log
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.appComponent
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import ru.megboyzz.dnevnik.util.collectAsMutableState
import ru.megboyzz.dnevnik.util.navigate
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.ui.AlertMessageBox
import ru.megboyzz.dnevnik.screens.ui.LoginScreenContent
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
//TODO Переверстать экран входа
@Composable
fun LoginScreen(navController: NavController) {
val login = remember {
mutableStateOf("")
Log.i("LoginScreen", "Called")
val activity = LocalContext.current as MainActivity
val authorizationViewModel: AuthorizationViewModel by activity.viewModels {
ViewModelFactory{
activity.appComponent.authViewModel
}
}
val password = remember {
mutableStateOf("")
val login = authorizationViewModel.login.collectAsMutableState()
val password = authorizationViewModel.password.collectAsMutableState()
val loginStatus = authorizationViewModel.loginStatus.collectAsMutableState()
val rememberMe = authorizationViewModel.rememberMe.collectAsMutableState()
var isError by remember{
mutableStateOf(true)
}
val isError = remember {
mutableStateOf(false)
}
val rememberMe = remember {
var isLoading by remember {
mutableStateOf(false)
}
LoginScreenContent(
login = login,
password = password,
isError = isError,
isRememberMe = rememberMe,
onSignIn = { navController.navigate(AppNavRoute.Marks.route) },
onSignUp = { /*TODO*/ },
onForgetPassword = { /* TODO */ }
)
if(loginStatus.value == AuthorizationStatus.LOGGING) isLoading = true
if(loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS && isError) {
AlertMessageBox(
title = "Информация",
alertText = "Неверный логин или пароль",
onClick = { isError =! isError }
)
}
if(loginStatus.value == AuthorizationStatus.MAINTAIN){
isLoading = false
AlertMessageBox(
title = "Ведутся профилактические работы",
alertText = "Прямо сейчас мы проводим важные обновления, чтобы стать ещё лучше и удобнее. Пожалуйста, проявите немного терпения!",
onClick = { authorizationViewModel.acceptAlert() }
)
}
if(loginStatus.value == AuthorizationStatus.NO_INTERNET){
isLoading = false
AlertMessageBox(
title = "Информация",
alertText = "Отсутствует подключение к сети",
onClick = { loginStatus.value = AuthorizationStatus.NOTHING }
)
}
if(loginStatus.value == AuthorizationStatus.LOGGED) {
Log.i("loginStatus", "logged")
navController.navigate(AppNavRoute.Marks)
}
Box(){
LoginScreenContent(
login = login.value,
onLoginChange = { login.value = it },
password = password.value,
onPasswordChange = { password.value = it },
isError = loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS,
onErrorChange = { },
isLoading = isLoading,
isRememberMe = rememberMe.value,
onRememberChange = { rememberMe.value = it },
onSignIn = {
if (loginStatus.value != AuthorizationStatus.LOGGED)
authorizationViewModel.login()
},
onSignUp = { /*TODO*/ },
onForgetPassword = { /* TODO */ }
)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)){
Button(onClick = {
login.value = "08012002@mail.ru"
password.value = "1282565121024QWEasdfZXC"
}) {
Text("Я")
}
Button(onClick = {
login.value = "kitsekaterina"
password.value = "k12345"
}) {
Text("Катя")
}
val scope = rememberCoroutineScope()
var isRemoving by remember{ mutableStateOf(false) }
Button(onClick = {
val globalUserContextDao = activity.appComponent.globalUserContextDao
val credentialsDao = activity.appComponent.credentialsDao
scope.launch(Dispatchers.IO) {
isRemoving = true
globalUserContextDao.deleteGlobalUserContext()
credentialsDao.deleteCredentials()
isRemoving = false
}
}) {
Text("Удалить данные входа")
}
if(isRemoving) CircularProgressIndicator()
}
}
}
@@ -3,8 +3,8 @@ package ru.megboyzz.dnevnik.screens
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.navigation.MarksSubScreenNavHost
import ru.megboyzz.dnevnik.screens.ui.main.MainScaffold
@@ -3,8 +3,8 @@ package ru.megboyzz.dnevnik.screens
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.ui.main.MainScaffold
@@ -12,9 +12,11 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.ui.UnderlinedText
import ru.megboyzz.dnevnik.screens.ui.main.SettingsScaffold
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.util.*
@Composable
fun SettingsScreen(
@@ -23,9 +25,7 @@ fun SettingsScreen(
) {
SettingsScaffold(
onLeave = {
//navController.navigate()
},
onLeave = { navController.navigate(AppNavRoute.Marks) },
scaffoldState = scaffoldState,
navController = navController
) {
@@ -1,2 +1,9 @@
package ru.megboyzz.dnevnik.screens.subscreens
import androidx.compose.runtime.Composable
@Composable
fun ByDayMarksScreen() {
}
@@ -1,2 +1,57 @@
package ru.megboyzz.dnevnik.screens.subscreens
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.appComponent
import ru.megboyzz.dnevnik.screens.ui.LastMarkCard
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
import ru.megboyzz.dnevnik.viewmodel.subscreen.LastMarksViewModel
@Composable
fun LastMarksSubScreen() {
val activity = LocalContext.current as MainActivity
val lastMarksViewModel: LastMarksViewModel by activity.viewModels {
ViewModelFactory{
activity.appComponent.lastMarksViewModel
}
}
val isLoading = lastMarksViewModel.isLoading.collectAsState()
val lastMarks = lastMarksViewModel.lastMarksList.collectAsState()
if(isLoading.value){
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
CircularProgressIndicator()
}
}else{
LazyColumn{
items(lastMarks.value){
LastMarkCard(markModel = it)
}
}
}
}
@@ -0,0 +1,11 @@
package ru.megboyzz.dnevnik.screens.subscreens.viewmodel.model
import java.util.*
data class MarkModel(
val subjectName: String,
val mark: String,
val date: Date,
val teacher: String,
val reason: String = "null"
)
@@ -1,5 +1,6 @@
package ru.megboyzz.dnevnik.screens.ui
import android.app.AlertDialog
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
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.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
@@ -20,11 +22,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.onFocusChanged
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
@@ -33,10 +37,16 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.subscreens.viewmodel.model.MarkModel
import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.util.SpacerHeight
import ru.megboyzz.dnevnik.util.SpacerWidth
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
//Рефакторинг компонентов: разбиение файла с компонентами на отдельные файлы
@@ -74,25 +84,29 @@ fun AlmostOutlinedText(
@Composable
fun ProfileCard(
textColor: Color = white,
painter: Painter
userProfileModel: UserProfileModel
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Image(
modifier = Modifier.padding(10.dp, 15.dp, 20.dp, 15.dp),
painter = painter, //TODO заменить динмической картинкой
contentDescription = "chel"
AsyncImage(
modifier = Modifier
.padding(10.dp, 15.dp, 20.dp, 15.dp)
.clip(RoundedCornerShape(25.dp))
.size(50.dp),
contentScale = ContentScale.Crop,
model = userProfileModel.avatarUrl,
contentDescription = ""
)
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(
text = "Имя и фамилия",
text = userProfileModel.toString(),
style = H1,
color = textColor
)
Text(
text = "Класс/Группа",
text = userProfileModel.groupName,
style = H1,
color = textColor
)
@@ -102,18 +116,20 @@ fun ProfileCard(
@Composable
fun MainTextField(
value: MutableState<String>,
isError: MutableState<Boolean> = remember { mutableStateOf(false) },
value: String,
onChange: (String) -> Unit,
isError: Boolean,
isLoading: Boolean = false,
onErrorChanged: (Boolean) -> Unit,
label: String = "",
keyboardType: KeyboardType = KeyboardType.Text,
onChange: (it: String) -> Unit = { value.value = it }
) {
var passwordVisible by rememberSaveable { mutableStateOf(false) }
val focusRequester = FocusRequester()
OutlinedTextField(
value = value.value,
value = value,
onValueChange = onChange,
label = {
Text(
@@ -126,8 +142,8 @@ fun MainTextField(
textStyle = H2,
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = dark,
unfocusedBorderColor = if (isError.value) falseColor else mainBlue,
focusedBorderColor = if (isError.value) falseColor else mainBlue,
unfocusedBorderColor = if (isError) falseColor else mainBlue,
focusedBorderColor = if (isError) falseColor else mainBlue,
focusedLabelColor = dark,
unfocusedLabelColor = dark,
cursorColor = dark
@@ -152,42 +168,50 @@ fun MainTextField(
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, "")
}
} else {
if(isLoading) LoginProgress()
}
},
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused)
if (isError.value) isError.value = false
if (isError) onErrorChanged(false)
}
.fillMaxWidth(),
)
}
@Composable
fun LoginProgress() {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = mainBlue
)
}
@Composable
fun MainCheckbox(
isChecked: MutableState<Boolean>,
isChecked: Boolean,
modifier: Modifier = Modifier,
size: Float = 24f,
checkedColor: Color = mainBlue,
uncheckedColor: Color = lightGray,
checkmarkColor: Color = white,
onValueChange: () -> Unit
onValueChange: (Boolean) -> Unit
) {
val checkboxColor: Color by animateColorAsState(if (isChecked.value) checkedColor else uncheckedColor)
val checkboxColor: Color by animateColorAsState(if (isChecked) checkedColor else uncheckedColor)
val density = LocalDensity.current
val duration = 200
Row(
modifier = modifier
.toggleable(
value = isChecked.value,
value = isChecked,
role = Role.Checkbox,
onValueChange = {
isChecked.value = !isChecked.value
onValueChange.invoke()
}
onValueChange = onValueChange
)
) {
Card(
@@ -202,7 +226,7 @@ fun MainCheckbox(
contentAlignment = Alignment.Center
) {
androidx.compose.animation.AnimatedVisibility(
visible = isChecked.value,
visible = isChecked,
enter = slideInHorizontally(
animationSpec = tween(duration)
) {
@@ -345,11 +369,7 @@ fun CardWithAverage(
@Composable
fun LastMarkCard(
subjectName: String,
mark: Int,
date: String, /* мейби стоит поменять на Date или инстанс */
teacher: String,
reason: String = "null"
markModel: MarkModel
) {
BaseCard {
Row(
@@ -362,29 +382,29 @@ fun LastMarkCard(
modifier = Modifier.width(200.dp)
) {
Text(
text = subjectName,
text = markModel.subjectName,
style = MainText
)
Text(
text = mark.toString(),
text = markModel.mark.toString(),
style = H1,
color = mainBlue
)
}
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = "${R.string.title_mark_recive.AsString()} $date",
text = "${R.string.title_mark_recive.AsString()} ${markModel.date}",
style = MainText,
color = dark
)
Text(
text = "${R.string.title_teacher_name.AsString()}: $teacher",
text = "${R.string.title_teacher_name.AsString()}: ${markModel.teacher}",
style = MainText,
color = dark
)
if(reason != "null")
if(markModel.reason != "null")
Text(
text = "${R.string.title_reason.AsString()} $reason",
text = "${R.string.title_reason.AsString()} ${markModel.reason}",
style = MainText,
color = dark
)
@@ -618,7 +638,8 @@ fun AlertMessageBox(
contentColor = white
),
modifier = Modifier
.defaultMinSize(170.dp).padding(bottom = 30.dp)
.defaultMinSize(170.dp)
.padding(bottom = 30.dp)
) {
Text(
text = R.string.title_ok.AsString(),
@@ -630,3 +651,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
)
}
}
@@ -15,8 +15,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.dark
@@ -19,13 +19,22 @@ import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.Mini
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.util.SpacerHeight
import ru.megboyzz.dnevnik.util.SpacerWidth
@Composable
fun LoginScreenContent(
login: MutableState<String>,
password: MutableState<String>,
isError: MutableState<Boolean>,
isRememberMe: MutableState<Boolean>,
login: String,
onLoginChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
isLoading: Boolean,
isError: Boolean,
onErrorChange: (Boolean) -> Unit,
isRememberMe: Boolean,
onRememberChange: (Boolean) -> Unit,
onSignIn: () -> Unit,
onSignUp: () -> Unit,
onForgetPassword: () -> Unit
@@ -65,14 +74,19 @@ fun LoginScreenContent(
MainTextField(
value = login,
label = R.string.title_login.AsString(),
isError = isError
isError = isError,
isLoading = isLoading,
onErrorChanged = onErrorChange,
onChange = onLoginChange
)
SpacerHeight(15.dp)
MainTextField(
value = password,
keyboardType = KeyboardType.Password,
label = R.string.title_password.AsString(),
isError = isError
isError = isError,
onErrorChanged = onErrorChange,
onChange = onPasswordChange
)
}
SpacerHeight(5.dp)
@@ -87,7 +101,10 @@ fun LoginScreenContent(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
MainCheckbox(isRememberMe) { }
MainCheckbox(
isChecked = isRememberMe,
onValueChange = onRememberChange
)
SpacerWidth(5.dp)
Text(
text = R.string.title_remember_me.AsString(),
@@ -19,11 +19,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.kizitonwose.calendar.core.daysOfWeek
import ru.megboyzz.dnevnik.util.navigate
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.SettingsScreen
import ru.megboyzz.dnevnik.screens.subscreens.viewmodel.model.MarkModel
import ru.megboyzz.dnevnik.screens.ui.*
import ru.megboyzz.dnevnik.screens.ui.calendar.NiceCalendar
import ru.megboyzz.dnevnik.screens.ui.main.DrawerContent
import ru.megboyzz.dnevnik.screens.ui.main.SettingsScaffold
import ru.megboyzz.dnevnik.screens.ui.piechart.PieChart
import ru.megboyzz.dnevnik.screens.ui.piechart.PieChartCard
@@ -68,7 +71,7 @@ fun text() {
@Composable
fun DrawerPrev() {
val scaffoldState = rememberScaffoldState()
val navController = rememberAnimatedNavController()
val navController = rememberNavController()
Box(Modifier.background(mainBlue)) {
DrawerContent(navController = navController, scaffoldState = scaffoldState)
}
@@ -169,17 +172,6 @@ fun PieChartCardPrev() {
)
}
@Preview
@Composable
fun LastMarkCardPrev() {
LastMarkCard(
subjectName = "Русский язык",
mark = 5,
date = "18.02.2023",
teacher = "Степанова А.С.",
reason = "контрольная"
)
}
@Preview
@Composable
@@ -364,7 +356,7 @@ fun ScaffoldTest() {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
SettingsScaffold(
onLeave = { /*TODO*/ },
onLeave = { navController.navigate(AppNavRoute.Marks) },
scaffoldState = scaffoldState,
navController = navController
) {
@@ -391,3 +383,17 @@ fun Test1() {
)
}
@Preview
@Composable
fun AlertTest() {
LastMarkCard(markModel = MarkModel(
subjectName = "Обществознание",
mark = 4.toString(),
teacher = "Расита",
reason = "Ответ на уроке",
date = Date("2023-04-13T00:00:00")
))
}
@@ -16,7 +16,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.navigate
import ru.megboyzz.dnevnik.util.navigate
import ru.megboyzz.dnevnik.navigation.BaseNavRote
import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.dark
@@ -27,7 +27,7 @@ import ru.megboyzz.dnevnik.ui.theme.white
fun SubScreenNavButton(
text: String,
isClicked: MutableState<Boolean>,
onCLick: () -> Unit
onClick: () -> Unit
) {
val colors = ButtonDefaults.buttonColors(
backgroundColor = if(isClicked.value) mainBlue else white,
@@ -36,7 +36,7 @@ fun SubScreenNavButton(
Button(
onClick = {
isClicked.value = !isClicked.value
onCLick.invoke()
onClick.invoke()
},
shape = RoundedCornerShape(20.dp),
border = BorderStroke(
@@ -80,8 +80,9 @@ fun SubScreenNavBar(
isClicked = data.isClicked
) {
for(d in list){
if(data != d)
if(data != d) {
d.isClicked.value = false
}
}
}
}
@@ -26,6 +26,10 @@ import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.util.rememberFirstMostVisibleMonth
import ru.megboyzz.dnevnik.util.week
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.Month
@@ -20,6 +20,7 @@ import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.util.*
@Composable
fun BottomBar(navController: NavController) {
@@ -1,130 +1,203 @@
package ru.megboyzz.dnevnik.screens.ui
package ru.megboyzz.dnevnik.screens.ui.main
import android.annotation.SuppressLint
import android.util.Log
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.navigation.AppNavRoute
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.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.util.*
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModel
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
@Composable
fun NiceContainerForDrawer(
content: @Composable () -> Unit
) {
val elevation = 16.dp
val shape = RoundedCornerShape(
topEnd = 20.dp,
bottomEnd = 20.dp
)
Box(
modifier = Modifier
.shadow(elevation)
.zIndex(1f)
.clip(shape)
) {
Box(
modifier = Modifier
.background(
color = mainBlue,
shape = shape
)
.clip(shape)
) {
content.invoke()
}
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun DrawerContent(
navController: NavController,
scaffoldState: ScaffoldState
) {
Log.i("DrawerContent", "Called")
val activity = LocalContext.current as MainActivity
val profileViewModel: ProfileViewModel by activity.viewModels{
ViewModelFactory{
activity.appComponent.profileViewModel
}
}
val authorizationViewModel: AuthorizationViewModel by activity.viewModels {
ViewModelFactory{
activity.appComponent.authViewModel
}
}
val profile = profileViewModel.profile.collectAsState()
val credentialsDao = activity.appComponent.credentialsDao
val globalUserContextDao = activity.appComponent.globalUserContextDao
var leaveAlertIsOpened by remember {
mutableStateOf(false)
}
var isLeavingProcess by remember {
mutableStateOf(false)
}
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.width(260.dp)
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.Start
){
SpacerHeight(10.dp)
ProfileCard(painter = R.drawable.ic_author.AsPainter())
SpacerHeight(15.dp)
AlmostOutlinedText(text = "Идет 3-я четверть")
SpacerHeight(20.dp)
if(leaveAlertIsOpened){
LeaveAlert(
isLoading = isLeavingProcess,
onAgree = {
scope.launch(Dispatchers.IO) {
isLeavingProcess = true
credentialsDao.deleteCredentials()
globalUserContextDao.deleteGlobalUserContext()
leaveAlertIsOpened = false
}
authorizationViewModel.resetAuth()
navController.navigate(AppNavRoute.Login)
},
onCancel = { leaveAlertIsOpened = false }
)
}
NiceContainerForDrawer {
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(5.dp, 0.dp)
modifier = Modifier
.width(260.dp)
.fillMaxSize()
.padding(10.dp)
.background(mainBlue),
horizontalAlignment = Alignment.Start,
) {
DrawerMainButton(
icon = R.drawable.ic_marks.AsPainter(),
text = R.string.title_marks.AsString(),
onClick = {
navController.navigate(AppNavRoute.Marks)
scope.launch { scaffoldState.drawerState.close() }
if(profile.value == null)
CircularProgressIndicator()
else
ProfileCard(userProfileModel = profile.value!!)
SpacerHeight(15.dp)
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)
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
DrawerMainButton(
icon = R.drawable.ic_marks.AsPainter(),
text = R.string.title_marks.AsString(),
onClick = {
navController.navigate(AppNavRoute.Marks)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_homework.AsPainter(),
text = R.string.title_hw.AsString(),
onClick = {
navController.navigate(AppNavRoute.HomeWorks)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_schedule.AsPainter(),
text = R.string.title_schedule.AsString(),
onClick = {
navController.navigate(AppNavRoute.Schedule)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_settings.AsPainter(),
text = R.string.title_settings.AsString(),
onClick = {
navController.navigate(AppNavRoute.Settings)
scope.launch { scaffoldState.drawerState.close() }
}
)
}
Spacer(Modifier.weight(1f))
Divider(color = white, thickness = 1.dp)
SpacerHeight(5.dp)
Column(Modifier.padding(5.dp, 0.dp)) {
DrawerMainButton(
icon = R.drawable.ic_exit.AsPainter(),
text = R.string.title_leave_from_acconut.AsString()
) {
leaveAlertIsOpened = true
}
)
DrawerMainButton(
icon = R.drawable.ic_homework.AsPainter(),
text = R.string.title_hw.AsString(),
onClick = {
navController.navigate(AppNavRoute.HomeWorks)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_schedule.AsPainter(),
text = R.string.title_schedule.AsString(),
onClick = {
navController.navigate(AppNavRoute.Schedule)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_settings.AsPainter(),
text = R.string.title_settings.AsString(),
onClick = {
navController.navigate(AppNavRoute.Settings)
scope.launch { scaffoldState.drawerState.close() }
}
)
}
Spacer(Modifier.weight(1f))
Divider(color = white, thickness = 1.dp)
SpacerHeight(5.dp)
Column(Modifier.padding(5.dp, 0.dp)) {
DrawerMainButton(
icon = R.drawable.ic_exit.AsPainter(),
text = R.string.title_leave_from_acconut.AsString()
) { /* TODO */ }
}
}
}
}
val drawerShape = object : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline.Rounded {
val radius = CornerRadius(30f, 30f)
val nullRadius = CornerRadius.Zero
return Outline.Rounded(
RoundRect(
left = 0f,
top = 0f,
right = size.width * 26 / 36,
bottom = size.height,
topLeftCornerRadius = nullRadius,
topRightCornerRadius = radius,
bottomRightCornerRadius = radius,
bottomLeftCornerRadius = nullRadius
)
)
}
}
@Composable
fun DrawerMainButton(
icon: Painter,
@@ -132,26 +205,23 @@ fun DrawerMainButton(
textColor: Color = white,
onClick: () -> Unit = {}
) {
Column(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.mainClickable(onClick = onClick),
verticalArrangement = Arrangement.Center
) {
Row(
horizontalArrangement = Arrangement.Start
){
Image(
painter = icon,
contentDescription = "icon"
)
SpacerWidth(13.dp)
Text(
text = text,
style = H2,
color = textColor
)
}
.mainClickable { onClick.invoke() }
){
Image(
painter = icon,
contentDescription = "icon",
Modifier.padding(start = 10.dp)
)
SpacerWidth(13.dp)
Text(
text = text,
style = H2,
color = textColor
)
}
}
@@ -1,10 +1,7 @@
package ru.megboyzz.dnevnik.screens.ui.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@@ -19,23 +16,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.SpacerWidth
import ru.megboyzz.dnevnik.screens.ui.DrawerContent
import ru.megboyzz.dnevnik.screens.ui.drawerShape
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.util.SpacerWidth
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.viewmodel.model.UserProfileModel
@Composable
fun MainScaffold(
icon: Painter,
title: String,
/* TODO viewModel */
navController: NavController,
scaffoldState: ScaffoldState,
content: (@Composable (it: PaddingValues) -> Unit),
content: @Composable (PaddingValues) -> Unit,
) {
val scope = rememberCoroutineScope()
@@ -55,6 +50,9 @@ fun MainScaffold(
navController = navController,
scaffoldState = scaffoldState,
bottomBar = { BottomBar(navController) },
drawerContent = {
DrawerContent(navController, scaffoldState)
},
content = content
)
}
@@ -68,6 +66,7 @@ fun BaseScaffold(
navController: NavController,
scaffoldState: ScaffoldState,
bottomBar: @Composable () -> Unit,
drawerContent: @Composable (ColumnScope) -> Unit = {},
content: (@Composable (it: PaddingValues) -> Unit),
) {
Scaffold(
@@ -99,10 +98,11 @@ fun BaseScaffold(
}
},
content = content,
drawerBackgroundColor = mainBlue,
drawerContentColor = white,
drawerShape = drawerShape,
drawerContent = { DrawerContent(navController, scaffoldState) },
drawerBackgroundColor = Color.Transparent,
drawerContentColor = Color.Transparent,
//drawerShape = drawerShape,
drawerElevation = 0.dp,
drawerContent = drawerContent,
bottomBar = bottomBar
)
}
@@ -2,13 +2,14 @@ package ru.megboyzz.dnevnik.screens.ui.piechart
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.R
@@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.ui.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
@@ -8,8 +8,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.AsPainter
import ru.megboyzz.dnevnik.AsString
import ru.megboyzz.dnevnik.util.AsPainter
import ru.megboyzz.dnevnik.util.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.screens.ui.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
@@ -0,0 +1,19 @@
package ru.megboyzz.dnevnik.service
import retrofit2.Call
import retrofit2.http.*
import ru.megboyzz.dnevnik.entities.response.UserContext
import ru.megboyzz.dnevnik.entities.teachers.Teacher
interface ContextService{
@GET("/mobile/v4/users/{personId}/context")
fun getContext(@Header("Access-Token") accessToken: String, @Path("personId") personId: String): Call<UserContext>
@GET("/v1/edu-groups/{groupId}/teachers")
fun getTeachers(
@Header("Access-Token") accessToken: String,
@Path("groupId") groupId: Long
): Call<List<Teacher>>
}
@@ -0,0 +1,22 @@
package ru.megboyzz.dnevnik.service
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import ru.megboyzz.dnevnik.entities.marks.RecentMarks
//Сервис для последних оценок
interface MarksService {
@GET("/v2/persons/{personId}/group/{groupId}/recentmarks")
fun getRecentMarks(
@Header("Access-Token") accessToken: String,
@Path("personId") personId: Long,
@Path("groupId") groupId: Long
): Call<RecentMarks>
}
@@ -0,0 +1,215 @@
package ru.megboyzz.dnevnik.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.scale
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.kizitonwose.calendar.compose.CalendarLayoutInfo
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarLayoutInfo
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import ru.megboyzz.dnevnik.ui.theme.dark
import java.time.DayOfWeek
import kotlin.coroutines.*
@Composable
fun Int.AsPainter() = painterResource(this)
@Composable
fun Int.AsString() = stringResource(this)
@Composable
fun SpacerWidth(width: Dp) = Spacer(Modifier.width(width))
@Composable
fun SpacerHeight(height: Dp) = Spacer(Modifier.height(height))
@Composable
fun Int.AsImageVector() = ImageVector.vectorResource(this)
fun Modifier.drawColoredShadow(
color: Color = Color.Black,
alpha: Float = 0.07f,
borderRadius: Dp = 0.dp,
offsetX: Dp = 0.dp,
offsetY: Dp = 0.dp,
blurRadius: Dp = 7.dp,
spread: Dp = 0.dp,
enabled: Boolean = true,
) = if(enabled) {
this.drawBehind {
val transparentColor = color.copy(alpha = 0.0f).toArgb()
val shadowColor = color.copy(alpha = alpha).toArgb()
this.drawIntoCanvas {
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
frameworkPaint.color = transparentColor
frameworkPaint.setShadowLayer(
blurRadius.toPx(),
offsetX.toPx(),
offsetY.toPx(),
shadowColor
)
it.save()
if(spread.value > 0) {
fun calcSpreadScale(spread: Float, childSize: Float): Float {
return 1f + ((spread / childSize) * 2f)
}
it.scale(
calcSpreadScale(spread.toPx(), this.size.width),
calcSpreadScale(spread.toPx(), this.size.height),
this.center.x,
this.center.y
)
}
it.drawRoundRect(
0f,
0f,
this.size.width,
this.size.height,
borderRadius.toPx(),
borderRadius.toPx(),
paint
)
it.restore()
}
}
} else this
fun Modifier.mainClickable(
radius: Dp = Dp.Unspecified,
onClick: () -> Unit
) = composed { this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
radius = radius,
color = dark
),
onClick = onClick
) }
@Composable
fun rememberFirstMostVisibleMonth(
state: CalendarState,
viewportPercent: Float = 50f,
): CalendarMonth {
val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) }
LaunchedEffect(state) {
snapshotFlow { state.layoutInfo.firstMostVisibleMonth(viewportPercent) }
.filterNotNull()
.collect { month -> visibleMonth.value = month }
}
return visibleMonth.value
}
@Composable
fun rememberFirstMostVisibleWeek(
state: WeekCalendarState,
viewportPercent: Float = 50f,
): Week {
val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleWeek) }
LaunchedEffect(state) {
snapshotFlow { state.layoutInfo.firstMostVisibleWeek(viewportPercent) }
.filterNotNull()
.collect { month -> visibleMonth.value = month }
}
return visibleMonth.value
}
private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? {
return if (visibleMonthsInfo.isEmpty()) {
null
} else {
val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f
visibleMonthsInfo.firstOrNull { itemInfo ->
if (itemInfo.offset < 0) {
itemInfo.offset + itemInfo.size >= viewportSize
} else {
itemInfo.size - itemInfo.offset >= viewportSize
}
}?.month
}
}
private fun WeekCalendarLayoutInfo.firstMostVisibleWeek(viewportPercent: Float = 50f): Week? {
return if (visibleWeeksInfo.isEmpty()) {
null
} else {
val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f
visibleWeeksInfo.firstOrNull { itemInfo ->
if (itemInfo.offset < 0) {
itemInfo.offset + itemInfo.size >= viewportSize
} else {
itemInfo.size - itemInfo.offset >= viewportSize
}
}?.week
}
}
fun CalendarDay.week(): Week{
val weekList = mutableListOf<WeekDay>()
val dayOrder = this.date.dayOfWeek.ordinal
var mondayLocalDate = this.date.minusDays(dayOrder.toLong())
DayOfWeek.values().forEach {
weekList.add(WeekDay(mondayLocalDate, WeekDayPosition.RangeDate))
mondayLocalDate = mondayLocalDate.plusDays(1)
}
return Week(weekList)
}
private class MutableStateAdapter<T>(
private val state: State<T>,
private val mutate: (T) -> Unit
) : MutableState<T> {
override var value: T
get() = state.value
set(value) {
mutate(value)
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { value = it }
}
// Flow
@Composable
fun <T> MutableStateFlow<T>.collectAsMutableState(
context: CoroutineContext = EmptyCoroutineContext
): MutableState<T> = MutableStateAdapter(
state = collectAsState(context),
mutate = { value = it }
)
@@ -0,0 +1,741 @@
package ru.megboyzz.dnevnik.ui.components
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.SpacerHeight
import ru.megboyzz.dnevnik.ui.SpacerWidth
import java.util.Date
//Рефакторинг компонентов: разбиение файла с компонентами на отдельные файлы
//Этот файл должен хранить только базовые реализации компонентов и общие компонеты компонеты
@Composable
fun AlmostOutlinedText(
text: String,
contentColor: Color = white,
textAlign: TextAlign = TextAlign.Center
){
Column() {
Divider(color = contentColor, thickness = 1.dp)
Column(
modifier = Modifier
.fillMaxWidth()
.height(30.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = text,
color = contentColor,
textAlign = textAlign,
style = H2,
modifier = Modifier
.fillMaxWidth()
)
}
Divider(color = contentColor, thickness = 1.dp)
}
}
@Composable
fun ProfileCard(
textColor: Color = white,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
// TODO добавить контент
AsyncImage(
modifier = Modifier
.padding(10.dp, 15.dp, 20.dp, 15.dp)
.clip(RoundedCornerShape(25.dp))
.size(50.dp),
contentScale = ContentScale.Crop,
model = "",
contentDescription = ""
)
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(
text = "Имя",
style = H1,
color = textColor
)
Text(
text = "Класс",
style = H1,
color = textColor
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTextField(
value: String,
onChange: (String) -> Unit,
isError: Boolean,
isLoading: Boolean = false,
onErrorChanged: (Boolean) -> Unit,
label: String = "",
keyboardType: KeyboardType = KeyboardType.Text,
) {
var passwordVisible by rememberSaveable { mutableStateOf(false) }
val focusRequester = FocusRequester()
OutlinedTextField(
value = value,
onValueChange = onChange,
label = {
Text(
text = label,
style = H2,
color = dark
)
},
shape = RoundedCornerShape(10.dp),
textStyle = H2,
colors = TextFieldDefaults.outlinedTextFieldColors(
//textColor = dark,
unfocusedBorderColor = if (isError) falseColor else mainBlue,
focusedBorderColor = if (isError) falseColor else mainBlue,
focusedLabelColor = dark,
unfocusedLabelColor = dark,
cursorColor = dark
),
maxLines = 1,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
visualTransformation =
if (keyboardType == KeyboardType.Password) {
if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation()
} else VisualTransformation.None,
trailingIcon = {
if (keyboardType == KeyboardType.Password) {
val image = if (passwordVisible)
Icons.Filled.Visibility
else Icons.Filled.VisibilityOff
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, "")
}
} else {
if(isLoading) LoginProgress()
}
},
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused)
if (isError) onErrorChanged(false)
}
.fillMaxWidth(),
)
}
@Composable
fun LoginProgress() {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = mainBlue
)
}
@Composable
fun MainCheckbox(
isChecked: Boolean,
modifier: Modifier = Modifier,
size: Float = 24f,
checkedColor: Color = mainBlue,
uncheckedColor: Color = lightGray,
checkmarkColor: Color = white,
onValueChange: (Boolean) -> Unit
) {
val checkboxColor: Color by animateColorAsState(
if (isChecked) checkedColor else uncheckedColor,
label = ""
)
val density = LocalDensity.current
val duration = 200
Row(
modifier = modifier
.toggleable(
value = isChecked,
role = Role.Checkbox,
onValueChange = onValueChange
)
) {
Card(
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp
),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, color = checkedColor),
) {
Box(
modifier = Modifier
.size(size.dp)
.background(checkboxColor),
contentAlignment = Alignment.Center
) {
androidx.compose.animation.AnimatedVisibility(
visible = isChecked,
enter = slideInHorizontally(
animationSpec = tween(duration)
) {
(size * -0.5).toInt()
} + expandHorizontally(
expandFrom = Alignment.Start,
animationSpec = tween(duration)
),
exit = fadeOut()
) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = checkmarkColor
)
}
}
}
}
}
@Composable
fun MainButton(
text: String,
radius: Dp = 8.dp,
onClick: () -> Unit
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = mainBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(radius),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = text,
style = H1
)
}
}
//Базовая карточка для оценок, карточек с диаграммами
@Composable
fun BaseCard(
minWidth: Dp = 300.dp,
//Слой над карточкой
nextLayer: @Composable () -> Unit = {},
content: @Composable () -> Unit
){
val shape = RoundedCornerShape(10.dp)
//Расположение слоя регулируется этим алигментом
Box(contentAlignment = Alignment.TopEnd) {
Box(
Modifier
.clip(shape)
.background(
color = white,
shape = shape
)
.defaultMinSize(minWidth)
.border(
border = BorderStroke(
width = 1.dp,
color = mainBlue,
),
shape = shape
)
) { content() }
nextLayer()
}
}
@Composable
fun AverageText(
title: String,
mark: String,
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = title,
style = Mini,
color = white
)
Text(
text = mark,
style = MainText,
color = white
)
}
}
@Composable
fun CardWithAverage(
averageMark: Float?,
resultMark: Float?,
minWidth: Dp = 300.dp,
content: @Composable () -> Unit
) {
BaseCard(
minWidth = minWidth,
nextLayer = {
Box(
modifier = Modifier
.size(100.dp, 50.dp)
.background(color = mainBlue, shape = RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp)),
){
Box(
modifier = Modifier.size(50.dp),
contentAlignment = Alignment.Center
) {
AverageText(
title = R.string.title_average_mark.AsString(),
mark = if(averageMark != null) "%.2f".format(averageMark).replace(",", ".") else "-"
)
}
}
Box(
modifier = Modifier
.size(50.dp)
.fillMaxSize()
.background(color = totalMarkColor, shape = RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp)),
contentAlignment = Alignment.Center
){
AverageText(
title = R.string.title_result_mark.AsString(),
mark = resultMark?.toString() ?: "-"
)
}
}
) { content() }
}
//TODO переместить это в слой данных
data class MarkModel(
val subjectName: String,
val mark: String,
val date: Date,
val teacher: String,
val reason: String = "null"
)
@Composable
fun LastMarkCard(
markModel: MarkModel
) {
BaseCard {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.padding(15.dp)
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.width(200.dp)
) {
Text(
text = markModel.subjectName,
style = MainText
)
Text(
text = markModel.mark.toString(),
style = H1,
color = mainBlue
)
}
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = "${R.string.title_mark_recive.AsString()} ${markModel.date}",
style = MainText,
color = dark
)
Text(
text = "${R.string.title_teacher_name.AsString()}: ${markModel.teacher}",
style = MainText,
color = dark
)
if(markModel.reason != "null")
Text(
text = "${R.string.title_reason.AsString()} ${markModel.reason}",
style = MainText,
color = dark
)
}
}
}
}
@Composable
fun UnderlinedText(
text: String
) {
Column {
Column(
modifier = Modifier
.fillMaxWidth()
.height(30.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = text,
color = dark,
textAlign = TextAlign.Center,
style = H1,
modifier = Modifier
.fillMaxWidth()
)
}
SpacerHeight(height = 2.dp)
Divider(color = dark, thickness = 1.dp)
}
}
@Composable
fun AllMarksCard(
subjectName: String,
teacher: String?,
marks: List<Int>?,
resultMark: Float?
) {
CardWithAverage(
averageMark = marks?.average()?.toFloat(),
resultMark = resultMark
) {
Box(Modifier.padding(15.dp, 15.dp)) {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = R.drawable.ic_subject.AsPainter(),
contentDescription = ""
)
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = subjectName,
color = dark,
style = MainText,
overflow = TextOverflow.Ellipsis
)
Text(
text = teacher ?: "-",
color = dark,
style = MainText,
overflow = TextOverflow.Ellipsis
)
}
}
Divider(
color = dark,
thickness = 1.dp,
modifier = Modifier.padding(
top = 15.dp,
bottom = 10.dp,
)
)
if(marks != null)
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 20.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(marks){
MarkInBox(mark = it)
}
}
else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth()
){
EmptyMessage(message = R.string.title_no_marks.AsString())
}
}
}
}
}
}
@Composable
fun MarkInBox(mark: Int) {
Box(
modifier = Modifier
.size(19.dp)
.fillMaxSize()
.background(
color = mainBlue,
shape = RoundedCornerShape(2.dp)
),
contentAlignment = Alignment.Center
){
Text(
text = mark.toString(),
color = white
)
}
}
@Composable
fun ContentMessage(
message: String,
isFine: Boolean = true
) {
Column(
modifier = Modifier
.defaultMinSize(200.dp)
.padding(25.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = if(isFine) R.drawable.ic_smile.AsPainter() else R.drawable.ic_sad_smile.AsPainter(),
contentDescription = ""
)
SpacerHeight(height = 10.dp)
Text(
text = message,
style = MainText,
color = dark
)
}
}
@Composable
fun EmptyMessage(message: String) = ContentMessage(message = message, isFine = false)
@Composable
fun FineMessage(message: String) = ContentMessage(message = message)
@Composable
fun MessageAlert(
content: @Composable () -> Unit
) {
BaseCard {
Box(
modifier = Modifier.defaultMinSize(300.dp),
contentAlignment = Alignment.Center,
){
content.invoke()
}
}
}
@Composable
fun MessageAlertInWork() {
MessageAlert {
EmptyMessage(message = "Раздел находится в разработке!")
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BaseAlertBox(
title: String?,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
buttons: @Composable () -> Unit
) {
//TODO переверстать
AlertDialog(
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
),
//shape = RoundedCornerShape(20.dp),
onDismissRequest = onDismissRequest,
){
if(title != null)
Text(
text = title,
style = H1
)
content()
buttons()
}
}
@Composable
fun AlertMessageBox(
title: String,
alertText: String,
onClick: () -> Unit,
onCancel: () -> Unit = {},
) {
BaseAlertBox(
title = title,
onDismissRequest = onCancel,
content = {
Text(
text = alertText,
style = H2,
color = dark
)
},
buttons = {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = mainBlue,
contentColor = white
),
modifier = Modifier
.defaultMinSize(170.dp)
.padding(bottom = 30.dp)
) {
Text(
text = R.string.title_ok.AsString(),
style = H1
)
}
}
}
)
}
@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(
containerColor = mainBlue,
contentColor = white
),
modifier = Modifier
.padding(bottom = 30.dp)
){
Text(
text = title,
style = H1
)
}
}
@@ -0,0 +1,156 @@
package ru.megboyzz.dnevnik.screens.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.components.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
@Composable
fun BaseHomeWorkCard(
subjectName: String,
homeWorkTaskText: String,
deadlineDate: LocalDate,
deadlineLesson: Int = -1,
isDone: Boolean,
onClickBox: (Boolean) -> Unit
) {
val deadlineLessonText = if(deadlineLesson != -1) "$deadlineLesson-го урока" else ""
val format = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.getDefault())
var expanded by remember {
mutableStateOf(false)
}
BaseCard {
Box(
modifier = Modifier
.defaultMinSize(300.dp)
.padding(5.dp)
){
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.defaultMinSize(500.dp)
){
HomeWorkIsDoneCheckBox(isDone = isDone, onClickBox = onClickBox)
Text(
text = subjectName,
style = H2,
color = dark,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
HomeWorkCardToggler(expanded = expanded, onChangeExpand = {expanded = !it})
}
if(expanded) {
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(5.dp)
) {
HomeworkText(text = homeWorkTaskText)
Text(
text = "${R.string.title_deadline.AsString()} ${
deadlineDate.format(
format
)
} $deadlineLessonText",
style = H2,
color = dark
)
}
}
}
}
}
}
@Composable
fun HomeWorkIsDoneCheckBox(
isDone: Boolean,
onClickBox: (Boolean) -> Unit
) {
val shape = RoundedCornerShape(9.dp)
Box(
modifier = Modifier
.size(25.dp, 20.dp)
.clip(shape)
.border(
border = BorderStroke(
width = 1.dp,
color = mainBlue
),
shape = shape
)
.clickable { onClickBox.invoke(isDone) },
contentAlignment = Alignment.Center
){
if(isDone)
Image(
painter = R.drawable.ic_done.AsPainter(),
contentDescription = ""
)
}
}
@Composable
fun HomeWorkCardToggler(
expanded: Boolean,
onChangeExpand: (Boolean) -> Unit
) {
Box(modifier = Modifier.size(24.dp)) {
IconButton(
onClick = { onChangeExpand.invoke(expanded) },
modifier = Modifier.fillMaxSize()
) {
Icon(
painter = (if (expanded) R.drawable.ic_hw_toggled else R.drawable.ic_hw_untoggled).AsPainter(),
contentDescription = ""
)
}
}
}
@Composable
fun HomeworkText(
text: String,
){
Column() {
Divider(color = dark, thickness = 1.dp, modifier = Modifier.width(500.dp))
Column(
modifier = Modifier
.defaultMinSize(500.dp)
.height(40.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = text,
color = dark,
style = H2,
)
}
Divider(color = dark, thickness = 1.dp, modifier = Modifier.width(500.dp))
}
}
@@ -0,0 +1,177 @@
package ru.megboyzz.dnevnik.screens.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.ui.theme.Big
import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.Mini
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.components.MainButton
import ru.megboyzz.dnevnik.ui.components.MainCheckbox
import ru.megboyzz.dnevnik.ui.components.MainTextField
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.SpacerHeight
import ru.megboyzz.dnevnik.ui.SpacerWidth
@Composable
fun LoginScreenContent(
login: String,
onLoginChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
isLoading: Boolean,
isError: Boolean,
onErrorChange: (Boolean) -> Unit,
isRememberMe: Boolean,
onRememberChange: (Boolean) -> Unit,
onSignIn: () -> Unit,
onSignUp: () -> Unit,
onForgetPassword: () -> Unit
) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp
var defaultPadding = 65.dp
val isNormal = screenWidth < 500
Box(Modifier.fillMaxSize()) {
if(!isNormal) BigLoginBackground()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
if (isNormal) LoginBackground()
else {
defaultPadding *= 4
}
Row(horizontalArrangement = Arrangement.Center) {
Text(
text = R.string.title_welcome.AsString(),
style = Big,
textAlign = TextAlign.Center,
modifier = Modifier.padding(defaultPadding, 20.dp, defaultPadding, 40.dp),
color = dark
)
}
Column(
modifier = Modifier.padding(defaultPadding, 0.dp)
) {
Column(Modifier.fillMaxWidth()) {
MainTextField(
value = login,
label = R.string.title_login.AsString(),
isError = isError,
isLoading = isLoading,
onErrorChanged = onErrorChange,
onChange = onLoginChange
)
SpacerHeight(15.dp)
MainTextField(
value = password,
keyboardType = KeyboardType.Password,
label = R.string.title_password.AsString(),
isError = isError,
onErrorChanged = onErrorChange,
onChange = onPasswordChange
)
}
SpacerHeight(5.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.height(30.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
MainCheckbox(
isChecked = isRememberMe,
onValueChange = onRememberChange
)
SpacerWidth(5.dp)
Text(
text = R.string.title_remember_me.AsString(),
style = MainText,
color = dark
)
}
Text(
text = R.string.title_forget_password.AsString(),
style = MainText,
color = dark,
modifier = Modifier.clickable(onClick = onForgetPassword)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(0.dp, 10.dp),
verticalArrangement = Arrangement.Center
) {
MainButton(
text = R.string.title_sign_in.AsString(),
onClick = onSignIn
)
SpacerHeight(5.dp)
Text(
text = R.string.title_sign_up.AsString(),
modifier = Modifier
.clickable(onClick = onSignUp)
.padding(5.dp)
.fillMaxWidth(),
textAlign = TextAlign.Center,
style = Mini,
color = dark
)
}
}
}
}
}
@Composable
fun LoginBackground() {
val icon = R.drawable.ic_header.AsPainter()
val size = icon.intrinsicSize
Column {
Image(
painter = icon,
contentDescription = "",
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth,
alignment = Alignment.TopStart
)
}
}
@Composable
fun BigLoginBackground() {
val icon = R.drawable.header_big.AsPainter()
Column(Modifier.fillMaxWidth()) {
Image(
painter = icon,
contentDescription = "",
modifier = Modifier.fillMaxWidth(),
alignment = Alignment.TopStart
)
}
}
@@ -0,0 +1,409 @@
package ru.megboyzz.dnevnik.screens.ui
import android.util.Log
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import com.kizitonwose.calendar.core.daysOfWeek
import ru.megboyzz.dnevnik.ui.navigate
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.SettingsScreen
import ru.megboyzz.dnevnik.screens.subscreens.viewmodel.model.MarkModel
import ru.megboyzz.dnevnik.screens.ui.*
import ru.megboyzz.dnevnik.ui.components.calendar.NiceCalendar
import ru.megboyzz.dnevnik.screens.ui.main.DrawerContent
import ru.megboyzz.dnevnik.screens.ui.main.SettingsScaffold
import ru.megboyzz.dnevnik.screens.ui.piechart.PieChart
import ru.megboyzz.dnevnik.screens.ui.piechart.PieChartCard
import ru.megboyzz.dnevnik.screens.ui.piechart.PieChartData
import ru.megboyzz.dnevnik.screens.ui.schedule.BaseScheduleCallsCard
import ru.megboyzz.dnevnik.screens.ui.schedule.BaseScheduleCard
import ru.megboyzz.dnevnik.screens.ui.schedule.BaseScheduleShield
import ru.megboyzz.dnevnik.screens.ui.schedule.CallData
import ru.megboyzz.dnevnik.ui.components.AlertMessageBox
import ru.megboyzz.dnevnik.ui.components.AllMarksCard
import ru.megboyzz.dnevnik.ui.components.AlmostOutlinedText
import ru.megboyzz.dnevnik.ui.components.CardWithAverage
import ru.megboyzz.dnevnik.ui.components.ContentMessage
import ru.megboyzz.dnevnik.ui.components.EmptyMessage
import ru.megboyzz.dnevnik.ui.components.FineMessage
import ru.megboyzz.dnevnik.ui.components.LastMarkCard
import ru.megboyzz.dnevnik.ui.components.MessageAlert
import ru.megboyzz.dnevnik.ui.components.UnderlinedText
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.TextStyle
import java.util.*
//Файл со всеми превьюшками
@Composable
fun NicePreviewContainer(
content: @Composable () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.height(1500.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) { content() }
}
@Preview
@Composable
fun text() {
Row(Modifier.fillMaxWidth()) {
AlmostOutlinedText(text = "Здарова карова")
}
}
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun DrawerPrev() {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
Box(Modifier.background(mainBlue)) {
DrawerContent(navController = navController, scaffoldState = scaffoldState)
}
}
@Preview
@Composable
fun CardWithAveragePrev() {
CardWithAverage(
averageMark = 4.33f,
resultMark = 4f
) {
Box(Modifier.padding(10.dp)){
Text("hehe")
}
}
}
@Preview
@Composable
fun BaseCardPrev() {
Box(
Modifier
.fillMaxSize()
){
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
Card(
backgroundColor = white,
shape = RoundedCornerShape(10.dp),
border = BorderStroke(
width = 1.dp,
color = mainBlue
),
modifier = Modifier.defaultMinSize(minWidth = 300.dp)
) {
Box(
Modifier
.size(50.dp)
.background(Color.Red)){}
}
}
}
}
@Preview
@Composable
fun UnderlinedTextPrev() {
Box(Modifier.background(white)){
UnderlinedText(text = "Общая сводка")
}
}
@Preview
@Composable
fun CirclePrev() {
val list = listOf(
PieChartData(legend = "История", value=5),
PieChartData(legend = "Русский язык", value=4),
PieChartData(legend = "Химия", value=1),
PieChartData(legend = "Математика", value=3)
)
PieChart(list)
}
@Preview
@Composable
fun GoodPieChartPrev() {
val list = listOf(
PieChartData(legend = "История", value=5),
PieChartData(legend = "Русский язык", value=4),
PieChartData(legend = "Химия", value=1),
PieChartData(legend = "Математика", value=3),
)
PieChart(diagramData = list)
}
@Preview
@Composable
fun PieChartCardPrev() {
val list = listOf(
PieChartData(legend = "История", value=5),
PieChartData(legend = "Русский язык", value=4),
PieChartData(legend = "Химия", value=1),
PieChartData(legend = "Математика", value=3),
)
PieChartCard(
title = "Сводка по оценкам отлично",
data = list
)
}
@Preview
@Composable
fun AllMarksCardPrev1() {
NicePreviewContainer {
AllMarksCard(
subjectName = "Математика",
teacher = "Степанова С.А.",
marks = listOf(4, 5, 5, 5, 5, 5, 4, 5, 3, 2),
resultMark = 4f
)
AllMarksCard(
subjectName = "Русский язык",
teacher = "Степанова С.А.",
marks = listOf(4, 5, 5, 5, 5, 5, 4, 5, 3, 2),
resultMark = null
)
AllMarksCard(
subjectName = "Русский язык",
teacher = "Степанова С.А.",
marks = null,
resultMark = 4.2f
)
AllMarksCard(
subjectName = "Русский язык",
teacher = null,
marks = listOf(4, 5, 5, 5, 5, 5, 4, 5, 3, 2),
resultMark = 4.2f
)
AllMarksCard(
subjectName = "Математика",
teacher = "Степанова С.А.",
marks = null,
resultMark = null
)
AllMarksCard(
subjectName = "Математика",
teacher = null,
marks = null,
resultMark = null
)
}
}
@Preview
@Composable
fun NiceCalendarPrev() {
var startMonth by remember{
mutableStateOf(YearMonth.now())
}
NiceCalendar(
currentMonth = startMonth,
){ day, month, year ->
}
}
@Preview
@Composable
fun EmptyPrev() {
ContentMessage(message = "Таких оценок нет")
}
@Preview
@Composable
fun PieChartNullPrev() {
val list = listOf(
PieChartData(legend = "История", value=5),
PieChartData(legend = "Русский язык", value=4),
PieChartData(legend = "Химия", value=1),
PieChartData(legend = "Математика", value=3),
)
NicePreviewContainer {
PieChartCard(
title = "Сводка по оценкам отлично",
data = list
)
PieChartCard(
title = "Сводка по оценкам хорошо",
data = null
)
}
}
@Preview
@Composable
fun MessagesPrev() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
MessageAlert {
EmptyMessage(message = "Таких оценок нет((")
}
MessageAlert {
FineMessage(message = "Сегодня нет уроков!")
}
}
}
@Preview
@Composable
fun HomeWorkCardPrev() {
var isDone by remember {
mutableStateOf(false)
}
BaseHomeWorkCard(
subjectName = "Алгебра и начала математического анализа",
homeWorkTaskText = "C. 45 №45, №56, №46",
deadlineDate = LocalDate.now(),
isDone = isDone,
onClickBox = {
isDone = !it
}
)
}
@Preview
@Composable
fun ScheduleShieldPrev() {
BaseScheduleShield(
subjectName = "Физика",
teacherName = "Иванов Иван Иванович",
classNumber = "3.13",
nextLessonName = "Обществознание"
)
}
@Preview
@Composable
fun ScheduleCardPrev() {
val daysOfWeek = daysOfWeek().dropLast(1)
Log.i("Locale", Locale.getDefault().country)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
){
items(daysOfWeek){
BaseScheduleCard(
dayTitle = it.getDisplayName(TextStyle.FULL, Locale.getDefault()),
subjectNames = listOf(
"Литература",
"Алгебра",
"Геометрия",
"Алгебра и начала математического анализа",
"Физика",
"Астрономия",
)
)
}
}
}
@Preview
@Composable
fun ScheduleCallsPrev() {
val calls = listOf(
CallData("9:00", "10:30"),
CallData("10:40", "12:10"),
CallData("12:40", "14:10"),
CallData("14:20", "15:50"),
CallData("18:00", "19:30")
)
BaseScheduleCallsCard(callsList = calls)
}
@Preview
@Composable
fun ScaffoldTest() {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
SettingsScaffold(
onLeave = { navController.navigate(AppNavRoute.Marks) },
scaffoldState = scaffoldState,
navController = navController
) {
}
}
@Preview
@Composable
fun SettingsTest() {
SettingsScreen(navController = rememberNavController(), scaffoldState = rememberScaffoldState())
}
@Preview
@Composable
fun Test1() {
AlertMessageBox(
title = "Информация",
alertText = "В данный момент на сервере ведутся технические работы, вход невозможен, повторите попытку позднее",
onClick = { /*TODO*/ }
)
}
@Preview
@Composable
fun AlertTest() {
LastMarkCard(markModel = MarkModel(
subjectName = "Обществознание",
mark = 4.toString(),
teacher = "Расита",
reason = "Ответ на уроке",
date = Date("2023-04-13T00:00:00")
))
}
@@ -0,0 +1,90 @@
package ru.megboyzz.dnevnik.screens.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.ui.navigate
import ru.megboyzz.dnevnik.navigation.BaseNavRote
import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
@Composable
fun SubScreenNavButton(
text: String,
isClicked: MutableState<Boolean>,
onClick: () -> Unit
) {
val colors = ButtonDefaults.buttonColors(
backgroundColor = if(isClicked.value) mainBlue else white,
contentColor = if(isClicked.value) white else dark
)
Button(
onClick = {
isClicked.value = !isClicked.value
onClick.invoke()
},
shape = RoundedCornerShape(20.dp),
border = BorderStroke(
width = 1.dp,
color = mainBlue,
),
colors = colors,
) {
Text(
text = text,
style= MainText
)
}
}
data class SubScreenData(
val title: String,
val to: BaseNavRote,
val isClicked: MutableState<Boolean>
)
@Composable
fun SubScreenNavBar(
list: List<SubScreenData>,
subScreenNavController: NavController
) {
val horizontalScroll = rememberScrollState()
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(horizontalScroll)
.padding(15.dp),
horizontalArrangement = Arrangement.spacedBy(15.dp)
){
for(data in list){
if(data.isClicked.value)
subScreenNavController.navigate(data.to)
SubScreenNavButton(
text = data.title,
isClicked = data.isClicked
) {
for(d in list){
if(data != d) {
d.isClicked.value = false
}
}
}
}
}
}
@@ -0,0 +1,351 @@
package ru.megboyzz.dnevnik.ui.components.calendar
import android.util.Log
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.HorizontalCalendar
import com.kizitonwose.calendar.compose.WeekCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import com.kizitonwose.calendar.core.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.*
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.rememberFirstMostVisibleMonth
import ru.megboyzz.dnevnik.ui.week
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.Month
import java.time.YearMonth
import java.time.format.TextStyle
import java.util.*
//TODO доделать свертку календаря, он почти идеально работает
@Composable
fun NiceCalendar(
currentMonth: YearMonth,
onClick: (day: CalendarDay, month: YearMonth, year: Int) -> Unit
) {
val selections = remember { mutableStateListOf<LocalDate>() }
val daysOfWeek = remember { daysOfWeek() }
var transitionEnabled by remember { mutableStateOf(true) }
val state = rememberCalendarState(
startMonth = startMonth,
endMonth = endMonth,
firstVisibleMonth = currentMonth,
firstDayOfWeek = daysOfWeek.first()
)
val weekState = rememberWeekCalendarState(
startDate = currentMonth.atDay(1),
endDate = currentMonth.atDay(7),
firstVisibleWeekDate = currentMonth.atDay(1),
firstDayOfWeek = daysOfWeek.first()
)
var selectedWeek by remember { mutableStateOf<Week?>(null) }
val monthScrollCoroutineScope = rememberCoroutineScope()
val weekScrollCoroutineScope = rememberCoroutineScope()
val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f)
Column {
SimpleCalendarTitle(
currentMonth = visibleMonth.yearMonth,
goToPrevious = {
state.scrollToPreviousMonth(monthScrollCoroutineScope)
},
goToNext = {
state.scrollToNextMonth(monthScrollCoroutineScope)
},
transitionEnabled = transitionEnabled
)
Card(
shape = RoundedCornerShape(10.dp),
modifier = Modifier.defaultMinSize(300.dp)
) {
if(selectedWeek == null) {
transitionEnabled = true
HorizontalCalendar(
state = state,
dayContent = { day ->
MonthDay(
day = day,
isSelected = selections.contains(day.date)
) { clickedDay ->
Log.i("Click!", "MonthDay is Clicked!")
//Чтобы два раза не вызывать week()
val week = clickedDay.week()
if(!selections.contains(day.date)) {
selections.removeIf { it != day.date }
selections.add(day.date)
}else selections.remove(day.date)
weekScrollCoroutineScope.launch {
weekState.startDate = week.days.first().date
weekState.endDate = week.days.last().date
weekState.animateScrollToWeek(clickedDay.date)
}
selectedWeek = if (selectedWeek == week) null else week
onClick(clickedDay, currentMonth, currentMonth.year)
}
},
monthHeader = { DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList()) },
modifier = Modifier.padding(10.dp)
)
}
else {
transitionEnabled = false
WeekCalendar(
state = weekState,
weekHeader = { _ ->
DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList())
},
dayContent = { day ->
WeekDay(
day = day,
isSelected = selections.contains(day.date)
) { calendarDay ->
Log.i("Click!", "WeekDay is Clicked!")
if(!selections.contains(day.date)) {
selections.removeIf { it != day.date }
selections.add(day.date)
}else selections.remove(day.date)
selectedWeek = null
}
},
modifier = Modifier.padding(10.dp)
)
}
}
}
}
fun CalendarState.scrollToPreviousMonth(coroutineScope: CoroutineScope){
val it = this
coroutineScope.launch {
it.scrollToMonth(it.firstVisibleMonth.yearMonth.previousMonth)
}
}
fun CalendarState.scrollToNextMonth(coroutineScope: CoroutineScope){
val it = this
coroutineScope.launch {
it.scrollToMonth(it.firstVisibleMonth.yearMonth.nextMonth)
}
}
@Composable
fun CalendarNavigationIcon(
icon: Painter,
contentDescription: String,
onClick: () -> Unit,
enabled: Boolean = true
) = Box(
modifier = Modifier
.size(35.dp)
.aspectRatio(1f)
.clip(shape = CircleShape)
.clickable(role = Role.Button, onClick = onClick, enabled = enabled),
) {
Icon(
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
.align(Alignment.Center),
painter = icon,
contentDescription = contentDescription,
)
}
@Composable
fun SimpleCalendarTitle(
modifier: Modifier = Modifier,
currentMonth: YearMonth,
goToPrevious: () -> Unit,
goToNext: () -> Unit,
transitionEnabled: Boolean = true
) {
val listOfMonth = listOf(
R.string.title_january.AsString(),
R.string.title_february.AsString(),
R.string.title_march.AsString(),
R.string.title_april.AsString(),
R.string.title_may.AsString(),
R.string.title_june.AsString(),
R.string.title_july.AsString(),
R.string.title_august.AsString(),
R.string.title_september.AsString(),
R.string.title_october.AsString(),
R.string.title_november.AsString(),
R.string.title_december.AsString()
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
contentAlignment = Alignment.Center,
) {
Row(
modifier = modifier
.height(40.dp)
.padding(vertical = 0.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
CalendarNavigationIcon(
icon = R.drawable.ic_month_swipe_left.AsPainter(),
contentDescription = "Previous",
onClick = goToPrevious,
enabled = transitionEnabled
)
Text(
text = currentMonth.displayText(listOfMonth),
style = H1,
color = dark
)
CalendarNavigationIcon(
icon = R.drawable.ic_month_swipe_right.AsPainter(),
contentDescription = "Next",
onClick = goToNext,
enabled = transitionEnabled
)
}
}
}
fun YearMonth.displayText(list: List<String>): String {
return "${list[this.month.ordinal]} ${this.year}"
}
fun Month.displayText(short: Boolean = true): String {
val style = if (short) TextStyle.SHORT else TextStyle.FULL_STANDALONE
return getDisplayName(style, Locale.getDefault())
}
fun DayOfWeek.displayText(uppercase: Boolean = false): String {
return getDisplayName(TextStyle.SHORT, Locale.getDefault()).let { value ->
if (uppercase) value.uppercase(Locale.getDefault()) else value
}
}
@Composable
fun DaysOfWeekTitle(daysOfWeek: List<DayOfWeek>) {
Column {
Row(modifier = Modifier.fillMaxWidth()) {
for (dayOfWeek in daysOfWeek) {
Text(
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
text = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()).uppercase(),
style = MainText
)
}
}
Divider(
thickness = 1.dp,
color = dark,
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
@Composable
fun MonthDay(
day: CalendarDay,
isSelected: Boolean,
onClick: (CalendarDay) -> Unit
) {
Day(day = day, isSelected = isSelected, onClick = onClick)
}
@Composable
fun WeekDay(
day: WeekDay,
isSelected: Boolean,
onClick: (CalendarDay) -> Unit
) {
Log.i("WeekDay", day.toString())
val position = when(day.position){
WeekDayPosition.InDate -> DayPosition.MonthDate
WeekDayPosition.OutDate -> DayPosition.MonthDate
WeekDayPosition.RangeDate -> DayPosition.MonthDate
}
val newDay = CalendarDay(day.date, position)
Day(day = newDay, isSelected = isSelected, onClick = onClick)
}
@Composable
fun Day(
day: CalendarDay,
isSelected: Boolean,
onClick: (CalendarDay) -> Unit
) {
Log.i("BaseDay", day.toString())
Box(
modifier = Modifier
.aspectRatio(ratio = 1f) // This is important for square sizing!
.clickable(
enabled = day.position == DayPosition.MonthDate,
onClick = { onClick(day) }
),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.background(color = if (isSelected) lightGray else Color.Transparent)
.border(
border = BorderStroke(
width = 1.dp,
color = if (isSelected) mainBlue else Color.Transparent
),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = day.date.dayOfMonth.toString(),
style = MainText,
textAlign = TextAlign.Justify,
color = if (day.position == DayPosition.MonthDate) dark else Color.Gray,
)
}
}
}
@@ -0,0 +1,8 @@
package ru.megboyzz.dnevnik.ui.components.calendar
import java.time.YearMonth
//Константы для календаря
val startMonth: YearMonth = YearMonth.of(2009, 9)
val endMonth: YearMonth = YearMonth.of(2100, 9)
@@ -0,0 +1,102 @@
package ru.megboyzz.dnevnik.screens.ui.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.drawColoredShadow
import ru.megboyzz.dnevnik.ui.mainClickable
import ru.megboyzz.dnevnik.util.*
@Composable
fun BottomBar(navController: NavController) {
Box(
modifier = Modifier
.fillMaxWidth()
.drawColoredShadow(
offsetY = 1.dp
)
.height(90.dp)
.background(white)
.padding(10.dp, 10.dp, 10.dp, 15.dp),
){
Box(
Modifier
.fillMaxSize()
.clip(RoundedCornerShape(20.dp))
.background(mainBlue)
){
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
BottomNavButton(
icon = R.drawable.ic_marks.AsPainter(),
text = R.string.title_marks.AsString(),
onClick = { navController.navigate(AppNavRoute.Marks) }
)
BottomNavButton(
icon = R.drawable.ic_homework.AsPainter(),
text = R.string.title_hw.AsString(),
onClick = { navController.navigate(AppNavRoute.HomeWorks) }
)
BottomNavButton(
icon = R.drawable.ic_schedule.AsPainter(),
text = R.string.title_schedule.AsString(),
onClick = { navController.navigate(AppNavRoute.Schedule) }
)
}
}
}
}
@Composable
fun BottomNavButton(
icon: Painter,
text: String,
color: Color = white,
onClick: () -> Unit,
) {
//Возможно стоит убрать анимацию клика
Box(
Modifier
.mainClickable(onClick = onClick, radius = 20.dp)
.padding(10.dp)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = icon,
contentDescription = "",
modifier = Modifier.padding(5.dp)
)
Text(
text = text,
style = MainText,
color = color,
overflow = TextOverflow.Ellipsis
)
}
}
}
@@ -0,0 +1,231 @@
package ru.megboyzz.dnevnik.screens.ui.main
import android.util.Log
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.SpacerHeight
import ru.megboyzz.dnevnik.ui.SpacerWidth
import ru.megboyzz.dnevnik.ui.components.AlmostOutlinedText
import ru.megboyzz.dnevnik.ui.components.LeaveAlert
import ru.megboyzz.dnevnik.ui.components.ProfileCard
import ru.megboyzz.dnevnik.ui.mainClickable
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.util.*
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.ProfileViewModel
import ru.megboyzz.dnevnik.viewmodel.core.ViewModelFactory
@Composable
fun NiceContainerForDrawer(
content: @Composable () -> Unit
) {
val elevation = 16.dp
val shape = RoundedCornerShape(
topEnd = 20.dp,
bottomEnd = 20.dp
)
Box(
modifier = Modifier
.shadow(elevation)
.zIndex(1f)
.clip(shape)
) {
Box(
modifier = Modifier
.background(
color = mainBlue,
shape = shape
)
.clip(shape)
) {
content.invoke()
}
}
}
@Composable
fun DrawerContent(
navController: NavController,
scaffoldState: ScaffoldState
) {
Log.i("DrawerContent", "Called")
val activity = LocalContext.current as MainActivity
val profileViewModel: ProfileViewModel by activity.viewModels{
ViewModelFactory{
activity.appComponent.profileViewModel
}
}
val authorizationViewModel: AuthorizationViewModel by activity.viewModels {
ViewModelFactory{
activity.appComponent.authViewModel
}
}
val profile = profileViewModel.profile.collectAsState()
val credentialsDao = activity.appComponent.credentialsDao
val globalUserContextDao = activity.appComponent.globalUserContextDao
var leaveAlertIsOpened by remember {
mutableStateOf(false)
}
var isLeavingProcess by remember {
mutableStateOf(false)
}
val scope = rememberCoroutineScope()
if(leaveAlertIsOpened){
LeaveAlert(
isLoading = isLeavingProcess,
onAgree = {
scope.launch(Dispatchers.IO) {
isLeavingProcess = true
credentialsDao.deleteCredentials()
globalUserContextDao.deleteGlobalUserContext()
leaveAlertIsOpened = false
}
authorizationViewModel.resetAuth()
navController.navigate(AppNavRoute.Login)
},
onCancel = { leaveAlertIsOpened = false }
)
}
NiceContainerForDrawer {
Column(
modifier = Modifier
.width(260.dp)
.fillMaxSize()
.padding(10.dp)
.background(mainBlue),
horizontalAlignment = Alignment.Start,
) {
if(profile.value == null)
CircularProgressIndicator()
else
ProfileCard(userProfileModel = profile.value!!)
SpacerHeight(15.dp)
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)
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
DrawerMainButton(
icon = R.drawable.ic_marks.AsPainter(),
text = R.string.title_marks.AsString(),
onClick = {
navController.navigate(AppNavRoute.Marks)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_homework.AsPainter(),
text = R.string.title_hw.AsString(),
onClick = {
navController.navigate(AppNavRoute.HomeWorks)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_schedule.AsPainter(),
text = R.string.title_schedule.AsString(),
onClick = {
navController.navigate(AppNavRoute.Schedule)
scope.launch { scaffoldState.drawerState.close() }
}
)
DrawerMainButton(
icon = R.drawable.ic_settings.AsPainter(),
text = R.string.title_settings.AsString(),
onClick = {
navController.navigate(AppNavRoute.Settings)
scope.launch { scaffoldState.drawerState.close() }
}
)
}
Spacer(Modifier.weight(1f))
Divider(color = white, thickness = 1.dp)
SpacerHeight(5.dp)
Column(Modifier.padding(5.dp, 0.dp)) {
DrawerMainButton(
icon = R.drawable.ic_exit.AsPainter(),
text = R.string.title_leave_from_acconut.AsString()
) {
leaveAlertIsOpened = true
}
}
}
}
}
@Composable
fun DrawerMainButton(
icon: Painter,
text: String,
textColor: Color = white,
onClick: () -> Unit = {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.mainClickable { onClick.invoke() }
){
Image(
painter = icon,
contentDescription = "icon",
Modifier.padding(start = 10.dp)
)
SpacerWidth(13.dp)
Text(
text = text,
style = H2,
color = textColor
)
}
}
@@ -0,0 +1,126 @@
package ru.megboyzz.dnevnik.screens.ui.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.SpacerWidth
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.white
import ru.megboyzz.dnevnik.R
@Composable
fun MainScaffold(
icon: Painter,
title: String,
navController: NavController,
scaffoldState: ScaffoldState,
content: @Composable (PaddingValues) -> Unit,
) {
val scope = rememberCoroutineScope()
BaseScaffold(
leadingIcon = Icons.Filled.Menu,
trailingIcon = icon,
onLeadingIconClick = {
val drawerState = scaffoldState.drawerState
scope.launch {
if (drawerState.isClosed)
drawerState.open()
else
drawerState.close()
}
},
title = title,
navController = navController,
scaffoldState = scaffoldState,
bottomBar = { BottomBar(navController) },
drawerContent = {
DrawerContent(navController, scaffoldState)
},
content = content
)
}
@Composable
fun BaseScaffold(
leadingIcon: ImageVector,
trailingIcon: Painter?,
onLeadingIconClick: () -> Unit,
title: String,
navController: NavController,
scaffoldState: ScaffoldState,
bottomBar: @Composable () -> Unit,
drawerContent: @Composable (ColumnScope) -> Unit = {},
content: (@Composable (it: PaddingValues) -> Unit),
) {
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
backgroundColor = mainBlue,
contentColor = Color.White
) {
IconButton(onClick = onLeadingIconClick) {
Icon(
imageVector = leadingIcon,
contentDescription = "",
tint = white
)
}
SpacerWidth(width = 5.dp)
Text(
text = title,
fontSize = 16.sp,
color = white
)
if(trailingIcon != null) {
Spacer(Modifier.weight(1f))
Box(Modifier.padding(15.dp, 15.dp)) {
Image(painter = trailingIcon, contentDescription = "icon")
}
}
}
},
content = content,
drawerBackgroundColor = Color.Transparent,
drawerContentColor = Color.Transparent,
//drawerShape = drawerShape,
drawerElevation = 0.dp,
drawerContent = drawerContent,
bottomBar = bottomBar
)
}
@Composable
fun SettingsScaffold(
onLeave: () -> Unit,
scaffoldState: ScaffoldState,
navController: NavController,
content: @Composable (PaddingValues) -> Unit
) {
BaseScaffold(
leadingIcon = Icons.Filled.ArrowBack,
trailingIcon = R.drawable.ic_settings.AsPainter(),
onLeadingIconClick = onLeave,
title = R.string.title_settings.AsString(),
navController = navController,
scaffoldState = scaffoldState,
bottomBar = {},
content = content
)
}
@@ -0,0 +1,58 @@
package ru.megboyzz.dnevnik.screens.ui.piechart
import android.content.Context
import android.graphics.Typeface
import android.widget.LinearLayout
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.PercentFormatter
class DataPieChart(context: Context) : PieChart(context) {
init {
layoutParams = LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
)
setUsePercentValues(true)
setEntryLabelTextSize(10f)
description.isEnabled = false
isDrawHoleEnabled = false
legend.isEnabled = false
legend.orientation = Legend.LegendOrientation.VERTICAL
legend.horizontalAlignment =
Legend.LegendHorizontalAlignment.CENTER
legend.textSize = 10f
setEntryLabelColor(Color.Black.toArgb())
}
fun updatePieChartWithData(data: List<PieChartData>) {
val entries = data.map { PieEntry(it.value.toFloat(), it.legend) }
val ds = PieDataSet(entries, "")
ds.colors = data.map { it.color.toArgb() }
ds.yValuePosition = PieDataSet.ValuePosition.INSIDE_SLICE
ds.xValuePosition = PieDataSet.ValuePosition.INSIDE_SLICE
ds.sliceSpace = 2f
ds.valueTextColor = Color.Black.toArgb()
ds.valueFormatter = PercentFormatter(this)
ds.valueTextSize = 10f
ds.valueTypeface = Typeface.DEFAULT
this.data = PieData(ds)
invalidate()
}
}
@@ -0,0 +1,84 @@
package ru.megboyzz.dnevnik.screens.ui.piechart
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.components.BaseCard
import ru.megboyzz.dnevnik.ui.components.EmptyMessage
/**
* Класс для предоставления данных для круговых диаграм
* Идея предоставить диаграмме массив именованных данных
* напрмер кол-во оцеок отлично по всем предметам:
* CircleDiagramData(parameterName = "История", count=5) = 38%
* CircleDiagramData(parameterName = "Русский язык", count=4) = 31%
* CircleDiagramData(parameterName = "Химия", count=1) = 8%
* CircleDiagramData(parameterName = "Математика", count=3) = 23%
*/
data class PieChartData(
val legend: String,
val value: Int
){
val color: Color
init {
val hue = (0..360).random(kotlin.random.Random(System.nanoTime())).toFloat()
Thread.sleep(10)
color = Color.hsl(hue, 0.5f, 0.6f)
}
}
@Composable
fun PieChart(
diagramData: List<PieChartData>
) {
lateinit var chart: DataPieChart
Column(Modifier.size(200.dp)) {
AndroidView(
factory = { context ->
chart = DataPieChart(context)
chart
},
modifier = Modifier
.wrapContentSize()
.padding(5.dp),
update = { chart.updatePieChartWithData(diagramData) }
)
}
}
@Composable
fun PieChartCard(
title: String,
data: List<PieChartData>?
) {
BaseCard {
Column(
modifier = Modifier.padding(15.dp, 10.dp),
){
Text(
text = title,
style = H2,
color = dark
)
Box(
modifier = Modifier.defaultMinSize(300.dp),
contentAlignment = Alignment.Center
){
if(data != null)
PieChart(diagramData = data)
else
EmptyMessage(message = R.string.title_no_marks.AsString())
}
}
}
}
@@ -0,0 +1,51 @@
package ru.megboyzz.dnevnik.screens.ui.schedule
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.components.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.dark
import java.util.*
//Веремнная сущность для определения пунктов карточки
data class CallData(
val startTime: String,
val endTime: String
)
@Composable
fun BaseScheduleCallsCard(
callsList: List<CallData>
) {
BaseCard{
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 20.dp).defaultMinSize(300.dp)
) {
for(i in callsList.indices)
Text(
text = "${(i+1).displayTextInRightLocale()} -> ${callsList[i].startTime} - ${callsList[i].endTime}",
style = H2,
color = dark
)
}
}
}
@Composable
fun Int.displayTextInRightLocale(): String
= if(Locale.getDefault().country == "RU")
"$this ${R.string.title_lesson.AsString()}"
else
"${R.string.title_lesson.AsString()} $this"
@@ -0,0 +1,52 @@
package ru.megboyzz.dnevnik.screens.ui.schedule
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.ui.components.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.mainBlue
import ru.megboyzz.dnevnik.ui.theme.textSchedule
@Composable
fun BaseScheduleCard(
dayTitle: String,
subjectNames: List<String>
) {
BaseCard {
Column(
modifier = Modifier.padding(10.dp).padding(bottom = 15.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = dayTitle,
style = H2,
color = mainBlue,
)
Divider(
color = mainBlue,
thickness = 1.dp,
)
}
for(i in subjectNames.indices){
Text(
text = "${i+1}. ${subjectNames[i]}",
style = textSchedule
)
}
}
}
}
@@ -0,0 +1,91 @@
package ru.megboyzz.dnevnik.screens.ui.schedule
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.megboyzz.dnevnik.ui.AsPainter
import ru.megboyzz.dnevnik.ui.AsString
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.components.BaseCard
import ru.megboyzz.dnevnik.ui.theme.H2
import ru.megboyzz.dnevnik.ui.theme.MainText
import ru.megboyzz.dnevnik.ui.theme.dark
import ru.megboyzz.dnevnik.ui.theme.mainBlue
@Composable
fun BaseScheduleShield(
subjectName: String,
teacherName: String,
classNumber: String,
nextLessonName: String
) {
BaseCard {
Column(
modifier = Modifier
.defaultMinSize(300.dp)
.padding(top = 20.dp, bottom = 40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = R.drawable.ic_cap.AsPainter(),
contentDescription = ""
)
TextHeader(
title = R.string.title_now_lesson.AsString(),
value = subjectName
)
}
TextHeader(
title = R.string.title_lesson_teacher.AsString(),
value = teacherName
)
TextHeader(
title = R.string.title_classroom.AsString(),
value = classNumber
)
TextHeader(
title = R.string.title_next_lesson.AsString(),
value = nextLessonName,
valueIsBlue = false
)
}
}
}
@Composable
fun TextHeader(
title: String,
value: String,
valueIsBlue: Boolean = true
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.width(200.dp)
) {
Text(
text = title,
style = MainText,
color = dark,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = value,
style = H2,
color = if(valueIsBlue) mainBlue else dark,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
@@ -1,4 +1,4 @@
package ru.megboyzz.dnevnik
package ru.megboyzz.dnevnik.util
import androidx.compose.foundation.clickable
@@ -28,12 +28,12 @@ import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarLayoutInfo
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.navigation.BaseNavRote
import ru.megboyzz.dnevnik.ui.theme.dark
import java.time.DayOfWeek
import kotlin.coroutines.*
@Composable
fun Int.AsPainter() = painterResource(this)
@@ -203,16 +203,28 @@ fun CalendarDay.week(): Week{
}
fun WeekDay.week(): Week{
val weekList = mutableListOf<WeekDay>()
val dayOrder = this.date.dayOfWeek.ordinal
var mondayLocalDate = this.date.minusDays(dayOrder.toLong())
DayOfWeek.values().forEach {
weekList.add(WeekDay(mondayLocalDate, WeekDayPosition.RangeDate))
mondayLocalDate = mondayLocalDate.plusDays(1)
}
return Week(weekList)
private class MutableStateAdapter<T>(
private val state: State<T>,
private val mutate: (T) -> Unit
) : MutableState<T> {
override var value: T
get() = state.value
set(value) {
mutate(value)
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { value = it }
}
// Flow
@Composable
fun <T> MutableStateFlow<T>.collectAsMutableState(
context: CoroutineContext = EmptyCoroutineContext
): MutableState<T> = MutableStateAdapter(
state = collectAsState(context),
mutate = { value = it }
)
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.util.gson
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
import java.util.*
class DateDeserializer : JsonDeserializer<Date> {
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): Date {
val date = json.asString
return Date()
}
}
@@ -0,0 +1,15 @@
package ru.megboyzz.dnevnik.util.gson
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
import java.util.*
class DateSerializer : JsonSerializer<Date> {
override fun serialize(date: Date?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
val formattedDate = date?.time?.let { "/Date($it)/" }
return JsonPrimitive(formattedDate)
}
}
@@ -0,0 +1,61 @@
package ru.megboyzz.dnevnik.viewmodel
import android.util.Log
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.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import java.lang.IllegalArgumentException
import javax.inject.Inject
class AuthorizationViewModel @Inject constructor(
private val authManager: AuthorizationManager
): ViewModel() {
val login = MutableStateFlow("")
val password = MutableStateFlow("")
val rememberMe = MutableStateFlow(false)
val loginStatus = MutableStateFlow(AuthorizationStatus.CREDENTIALS_LOADING)
init {
Log.i("AuthorizationViewModel", "AuthorizationViewModel in constructor")
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(authManager.checkCredentials())
}
}
fun login(){
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(AuthorizationStatus.LOGGING)
val login1 = authManager.login(login.value, password.value, rememberMe.value)
Log.i("Auth", login1.toString())
loginStatus.emit(login1)
}
}
fun resetAuth(){
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(AuthorizationStatus.LOGOUT)
login.emit("")
password.emit("")
}
}
fun acceptAlert(){
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(AuthorizationStatus.NOTHING)
}
}
}

Some files were not shown because too many files have changed in this diff Show More