mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
1.8.0
Удалёно большинство классов относящихся к CustomLessonViewService: - AlarmReceiver - BootCompletedBroadcastReceiver - ScheduleClvAlarm - StartClvService CustomLessonViewService теперь запускается сервером в определённое время. Переработаны формы авторизации и регистрации. В форме регистрации теперь можно выбрать свою группу из выпадающего списка, а не вводить вручную. Исправлен недочёт, из-за которого можно было вернуться к форме авторизации нажимая кнопку назад (или делать свайп для того же эффекта). Немного изменён логотип приложения. Изменена иконка уведомлений на самодельную.
This commit is contained in:
19
.idea/appInsightsSettings.xml
generated
19
.idea/appInsightsSettings.xml
generated
@@ -15,8 +15,27 @@
|
|||||||
<option name="projectNumber" value="946974192625" />
|
<option name="projectNumber" value="946974192625" />
|
||||||
</ConnectionSetting>
|
</ConnectionSetting>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="devices">
|
||||||
|
<list>
|
||||||
|
<DeviceSetting>
|
||||||
|
<option name="deviceType" value="Phone" />
|
||||||
|
<option name="displayName" value="Xiaomi (2311DRK48G)" />
|
||||||
|
<option name="manufacturer" value="Xiaomi" />
|
||||||
|
<option name="model" value="2311DRK48G" />
|
||||||
|
</DeviceSetting>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="versions">
|
||||||
|
<list>
|
||||||
|
<VersionSetting>
|
||||||
|
<option name="buildVersion" value="13" />
|
||||||
|
<option name="displayName" value="1.7.1 (13)" />
|
||||||
|
<option name="displayVersion" value="1.7.1" />
|
||||||
|
</VersionSetting>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
<option name="visibilityType" value="ALL" />
|
<option name="visibilityType" value="ALL" />
|
||||||
</InsightsFilterSettings>
|
</InsightsFilterSettings>
|
||||||
</value>
|
</value>
|
||||||
|
|||||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2024-10-10T18:25:08.106861775Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=482a22d" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId = "ru.n08i40k.polytechnic.next"
|
applicationId = "ru.n08i40k.polytechnic.next"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 13
|
versionCode = 15
|
||||||
versionName = "1.7.1"
|
versionName = "1.8.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -12,31 +12,17 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<!-- For schedule CLV service-->
|
|
||||||
<!-- <uses-permission android:name="android.permission.USE_EXACT_ALARM" />-->
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".PolytechnicApplication"
|
android:name=".PolytechnicApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.PolytechnicNext"
|
android:theme="@style/Theme.PolytechnicNext"
|
||||||
tools:targetApi="35">
|
tools:targetApi="35">
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.BootCompletedBroadcastReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name=".receiver.AlarmReceiver" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.MyFirebaseMessagingService"
|
android:name=".service.MyFirebaseMessagingService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,20 +1,13 @@
|
|||||||
package ru.n08i40k.polytechnic.next
|
package ru.n08i40k.polytechnic.next
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
import ru.n08i40k.polytechnic.next.data.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.model.Group
|
|
||||||
import ru.n08i40k.polytechnic.next.receiver.AlarmReceiver
|
|
||||||
import ru.n08i40k.polytechnic.next.utils.or
|
import ru.n08i40k.polytechnic.next.utils.or
|
||||||
import java.util.Calendar
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -34,84 +27,4 @@ class PolytechnicApplication : Application() {
|
|||||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
== PackageManager.PERMISSION_GRANTED)
|
== PackageManager.PERMISSION_GRANTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDate(group: Group): Calendar? {
|
|
||||||
val javaCalendar = Calendar.getInstance()
|
|
||||||
val currentMinutes = javaCalendar.get(Calendar.HOUR_OF_DAY) * 60 +
|
|
||||||
javaCalendar.get(Calendar.MINUTE)
|
|
||||||
var startDayIdx = javaCalendar.get(Calendar.DAY_OF_WEEK) - 2
|
|
||||||
|
|
||||||
println("Current day is $startDayIdx")
|
|
||||||
|
|
||||||
val currentDay = group.days[startDayIdx]
|
|
||||||
if (currentDay != null) {
|
|
||||||
val firstLesson = currentDay.first
|
|
||||||
|
|
||||||
if (firstLesson == null || firstLesson.time.start < currentMinutes) {
|
|
||||||
println("Current day already started or ended!")
|
|
||||||
++startDayIdx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (dayIdx in startDayIdx..5) {
|
|
||||||
println("Trying $dayIdx day...")
|
|
||||||
val day = group.days[dayIdx] ?: continue
|
|
||||||
println("Day isn't null")
|
|
||||||
val firstLesson = day.first ?: continue
|
|
||||||
println("Day isn't empty")
|
|
||||||
|
|
||||||
val executeMinutes = (firstLesson.time.start - 15).coerceAtLeast(0)
|
|
||||||
|
|
||||||
println("Schedule minutes at $executeMinutes")
|
|
||||||
|
|
||||||
return Calendar.getInstance().apply {
|
|
||||||
set(Calendar.DAY_OF_WEEK, dayIdx + 2) // sunday is first + index from 0
|
|
||||||
set(Calendar.HOUR_OF_DAY, executeMinutes / 60)
|
|
||||||
set(Calendar.MINUTE, executeMinutes % 60)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
// set(Calendar.MINUTE, get(Calendar.MINUTE) + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scheduleClvService(group: Group) {
|
|
||||||
// -1 = вс
|
|
||||||
// 0 = пн
|
|
||||||
// 1 = вт
|
|
||||||
// 2 = ср
|
|
||||||
// 3 = чт
|
|
||||||
// 4 = пт
|
|
||||||
// 5 = сб
|
|
||||||
|
|
||||||
println("Getting date...")
|
|
||||||
|
|
||||||
val date = getDate(group) ?: return
|
|
||||||
|
|
||||||
println("Alarm on this week!")
|
|
||||||
|
|
||||||
val alarmManager = applicationContext
|
|
||||||
.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
|
|
||||||
|
|
||||||
val pendingIntent =
|
|
||||||
Intent(applicationContext, AlarmReceiver::class.java).let {
|
|
||||||
PendingIntent.getBroadcast(
|
|
||||||
applicationContext,
|
|
||||||
IntentRequestCodes.ALARM_CLV,
|
|
||||||
it,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alarmManager != null)
|
|
||||||
println("Alarm manager isn't null.")
|
|
||||||
|
|
||||||
alarmManager?.cancel(pendingIntent)
|
|
||||||
alarmManager?.set(
|
|
||||||
AlarmManager.RTC_WAKEUP,
|
|
||||||
date.timeInMillis,
|
|
||||||
pendingIntent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,30 +2,18 @@ package ru.n08i40k.polytechnic.next.data.schedule.impl
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.Group
|
import ru.n08i40k.polytechnic.next.model.Group
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
||||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
|
||||||
|
|
||||||
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
|
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
|
||||||
override suspend fun getGroup(): MyResult<Group> {
|
override suspend fun getGroup(): MyResult<Group> =
|
||||||
return withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val groupName = runBlocking {
|
|
||||||
context.settingsDataStore.data.map { settings -> settings.group }.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupName.isEmpty())
|
|
||||||
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
|
|
||||||
|
|
||||||
val response = tryFuture {
|
val response = tryFuture {
|
||||||
ScheduleGet(
|
ScheduleGet(
|
||||||
ScheduleGet.RequestDto(groupName),
|
|
||||||
context,
|
context,
|
||||||
it,
|
it,
|
||||||
it
|
it
|
||||||
@@ -37,5 +25,4 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
|
|||||||
is MyResult.Success -> MyResult.Success(response.data.group)
|
is MyResult.Success -> MyResult.Success(response.data.group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,6 @@ open class RequestBase(
|
|||||||
override fun getHeaders(): MutableMap<String, String> {
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
val headers = mutableMapOf<String, String>()
|
val headers = mutableMapOf<String, String>()
|
||||||
headers["Content-Type"] = "application/json; charset=utf-8"
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
headers["version"] = "1"
|
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,10 @@ fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
|
|||||||
MyResult.Failure(exception)
|
MyResult.Failure(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unwrapException(exception: Exception): Throwable {
|
||||||
|
if (exception is ExecutionException && exception.cause != null)
|
||||||
|
return exception.cause!!
|
||||||
|
|
||||||
|
return exception
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ open class CachedRequest(
|
|||||||
private val listener: Response.Listener<String>,
|
private val listener: Response.Listener<String>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
) : AuthorizedRequest(context, method, url, {
|
) : AuthorizedRequest(context, method, url, {
|
||||||
runBlocking {
|
runBlocking(Dispatchers.IO) {
|
||||||
(context as PolytechnicApplication)
|
(context as PolytechnicApplication)
|
||||||
.container.networkCacheRepository.put(url, it)
|
.container.networkCacheRepository.put(url, it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
|
||||||
class AuthLogin(
|
class AuthSignIn(
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
context: Context,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
@@ -23,9 +23,16 @@ class AuthLogin(
|
|||||||
data class RequestDto(val username: String, val password: String)
|
data class RequestDto(val username: String, val password: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResponseDto(val id: String, val accessToken: String)
|
data class ResponseDto(val id: String, val accessToken: String, val group: String)
|
||||||
|
|
||||||
override fun getBody(): ByteArray {
|
override fun getBody(): ByteArray {
|
||||||
return Json.encodeToString(data).toByteArray()
|
return Json.encodeToString(data).toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
|
val headers = super.getHeaders()
|
||||||
|
headers["version"] = "2"
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
|
||||||
class AuthRegister(
|
class AuthSignUp(
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
context: Context,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
@@ -3,13 +3,11 @@ package ru.n08i40k.polytechnic.next.network.request.schedule
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.model.Group
|
import ru.n08i40k.polytechnic.next.model.Group
|
||||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||||
|
|
||||||
class ScheduleGet(
|
class ScheduleGet(
|
||||||
private val data: RequestDto,
|
|
||||||
context: Context,
|
context: Context,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
@@ -29,8 +27,4 @@ class ScheduleGet(
|
|||||||
val group: Group,
|
val group: Group,
|
||||||
val lastChangedDays: ArrayList<Int>,
|
val lastChangedDays: ArrayList<Int>,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getBody(): ByteArray {
|
|
||||||
return Json.encodeToString(data).toByteArray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,11 @@ class ScheduleGetCacheStatus(
|
|||||||
val lastCacheUpdate: Long,
|
val lastCacheUpdate: Long,
|
||||||
val lastScheduleUpdate: Long,
|
val lastScheduleUpdate: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
|
val headers = super.getHeaders()
|
||||||
|
headers["version"] = "1"
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,13 @@ import android.content.Context
|
|||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
|
||||||
class ScheduleGetGroupNames(
|
class ScheduleGetGroupNames(
|
||||||
context: Context,
|
context: Context,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : CachedRequest(
|
) : RequestBase(
|
||||||
context,
|
context,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"schedule/get-group-names",
|
"schedule/get-group-names",
|
||||||
|
|||||||
@@ -25,4 +25,11 @@ class ScheduleUpdate(
|
|||||||
override fun getBody(): ByteArray {
|
override fun getBody(): ByteArray {
|
||||||
return Json.encodeToString(data).toByteArray()
|
return Json.encodeToString(data).toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
|
val headers = super.getHeaders()
|
||||||
|
headers["version"] = "1"
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package ru.n08i40k.polytechnic.next.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.work.Constraints
|
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
|
|
||||||
import ru.n08i40k.polytechnic.next.work.ScheduleClvAlarm
|
|
||||||
|
|
||||||
class AlarmReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
println("Hi from AlarmReceiver")
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
println("No intend provided!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
println("No context provided!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
println(intent.action)
|
|
||||||
|
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val rescheduleRequest = OneTimeWorkRequestBuilder<ScheduleClvAlarm>()
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(context)
|
|
||||||
.enqueue(rescheduleRequest)
|
|
||||||
|
|
||||||
CurrentLessonViewService.startService(context.applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package ru.n08i40k.polytechnic.next.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.work.Constraints
|
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import ru.n08i40k.polytechnic.next.work.ScheduleClvAlarm
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
class BootCompletedBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
val logger = Logger.getLogger("BootCompletedBroadcastReceiver")
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
logger.warning("No context provided!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
logger.warning("No intend provided!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.action != "android.intent.action.BOOT_COMPLETED") {
|
|
||||||
logger.warning("Strange intent action passed!")
|
|
||||||
logger.warning(intent.action)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = OneTimeWorkRequestBuilder<ScheduleClvAlarm>()
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(context)
|
|
||||||
.enqueue(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,25 +3,24 @@ package ru.n08i40k.polytechnic.next.service
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.core.content.ContextCompat.startForegroundService
|
||||||
import androidx.work.WorkManager
|
|
||||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||||
import ru.n08i40k.polytechnic.next.model.Day
|
import ru.n08i40k.polytechnic.next.model.Day
|
||||||
import ru.n08i40k.polytechnic.next.model.Group
|
import ru.n08i40k.polytechnic.next.model.Group
|
||||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||||
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
|
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
|
||||||
import ru.n08i40k.polytechnic.next.utils.getDayMinutes
|
import ru.n08i40k.polytechnic.next.utils.getDayMinutes
|
||||||
import ru.n08i40k.polytechnic.next.work.StartClvService
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
class CurrentLessonViewService : Service() {
|
class CurrentLessonViewService : Service() {
|
||||||
@@ -30,19 +29,31 @@ class CurrentLessonViewService : Service() {
|
|||||||
private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
|
private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
|
||||||
private const val UPDATE_INTERVAL = 60_000L
|
private const val UPDATE_INTERVAL = 60_000L
|
||||||
|
|
||||||
fun startService(appContext: Context) {
|
suspend fun startService(application: PolytechnicApplication) {
|
||||||
if (!(appContext as PolytechnicApplication).hasNotificationPermission())
|
if (!application.hasNotificationPermission())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (Calendar.getInstance()
|
val schedule =
|
||||||
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
|
application
|
||||||
.get(Calendar.MINUTE) < 420)
|
.container
|
||||||
|
.scheduleRepository
|
||||||
|
.getGroup()
|
||||||
|
|
||||||
|
if (schedule is MyResult.Failure)
|
||||||
return
|
return
|
||||||
|
|
||||||
val request = OneTimeWorkRequestBuilder<StartClvService>()
|
val intent = Intent(application, CurrentLessonViewService::class.java)
|
||||||
.build()
|
.apply {
|
||||||
|
putExtra("group", (schedule as MyResult.Success).data)
|
||||||
|
}
|
||||||
|
|
||||||
WorkManager.getInstance(appContext).enqueue(request)
|
application.stopService(
|
||||||
|
Intent(
|
||||||
|
application,
|
||||||
|
CurrentLessonViewService::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
startForegroundService(application, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +62,10 @@ class CurrentLessonViewService : Service() {
|
|||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private val updateRunnable = object : Runnable {
|
private val updateRunnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
|
val logger = Logger.getLogger("CLV.updateRunnable")
|
||||||
|
|
||||||
if (day == null || day!!.nonNullIndices.isEmpty()) {
|
if (day == null || day!!.nonNullIndices.isEmpty()) {
|
||||||
|
logger.warning("Stopping, because day is null or empty!")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,7 +88,7 @@ class CurrentLessonViewService : Service() {
|
|||||||
if (currentLesson == null && nextLesson == null) {
|
if (currentLesson == null && nextLesson == null) {
|
||||||
val notification = NotificationCompat
|
val notification = NotificationCompat
|
||||||
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
||||||
.setSmallIcon(R.drawable.logo)
|
.setSmallIcon(R.drawable.schedule)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setContentTitle(getString(R.string.lessons_end_notification_title))
|
.setContentTitle(getString(R.string.lessons_end_notification_title))
|
||||||
.setContentText(getString(R.string.lessons_end_notification_description))
|
.setContentText(getString(R.string.lessons_end_notification_description))
|
||||||
@@ -146,7 +160,7 @@ class CurrentLessonViewService : Service() {
|
|||||||
): Notification {
|
): Notification {
|
||||||
return NotificationCompat
|
return NotificationCompat
|
||||||
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
||||||
.setSmallIcon(R.drawable.logo)
|
.setSmallIcon(R.drawable.schedule)
|
||||||
.setContentTitle(title ?: getString(R.string.lesson_notification_title))
|
.setContentTitle(title ?: getString(R.string.lesson_notification_title))
|
||||||
.setContentText(description ?: getString(R.string.lesson_notification_description))
|
.setContentText(description ?: getString(R.string.lesson_notification_description))
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
@@ -159,26 +173,29 @@ class CurrentLessonViewService : Service() {
|
|||||||
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSchedule(group: Group?): Boolean {
|
private fun updateSchedule(group: Group?) {
|
||||||
|
val logger = Logger.getLogger("CLV")
|
||||||
|
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
|
logger.warning("Stopping, because group is null")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentDay = group.current
|
val currentDay = group.current
|
||||||
if (currentDay == null || currentDay.nonNullIndices.isEmpty()) {
|
if (currentDay == null || currentDay.nonNullIndices.isEmpty()) {
|
||||||
|
logger.warning("Stopping, because current day is null or empty")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.day == null) {
|
|
||||||
val nowMinutes = Calendar.getInstance().getDayMinutes()
|
val nowMinutes = Calendar.getInstance().getDayMinutes()
|
||||||
|
if (nowMinutes < ((5 * 60) + 30)
|
||||||
if (currentDay.first!!.time.start - nowMinutes > 30
|
|| currentDay.last!!.time.end < nowMinutes
|
||||||
|| currentDay.last!!.time.end < nowMinutes) {
|
) {
|
||||||
|
logger.warning("Stopping, because service started outside of acceptable time range!")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return false
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.day = currentDay
|
this.day = currentDay
|
||||||
@@ -186,7 +203,7 @@ class CurrentLessonViewService : Service() {
|
|||||||
this.handler.removeCallbacks(updateRunnable)
|
this.handler.removeCallbacks(updateRunnable)
|
||||||
updateRunnable.run()
|
updateRunnable.run()
|
||||||
|
|
||||||
return true
|
logger.info("Running...")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -201,7 +218,7 @@ class CurrentLessonViewService : Service() {
|
|||||||
val notification = createNotification()
|
val notification = createNotification()
|
||||||
startForeground(NOTIFICATION_STATUS_ID, notification)
|
startForeground(NOTIFICATION_STATUS_ID, notification)
|
||||||
|
|
||||||
if (!updateSchedule(
|
updateSchedule(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
intent.getParcelableExtra("group", Group::class.java)
|
intent.getParcelableExtra("group", Group::class.java)
|
||||||
} else {
|
} else {
|
||||||
@@ -209,8 +226,6 @@ class CurrentLessonViewService : Service() {
|
|||||||
intent.getParcelableExtra("group")
|
intent.getParcelableExtra("group")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
updateRunnable.run()
|
|
||||||
|
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,19 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||||
|
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
|
import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
|
||||||
import ru.n08i40k.polytechnic.next.work.ScheduleClvAlarm
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
|
|
||||||
@@ -79,7 +85,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notify(id.hashCode(), notification)
|
notify(id.hashCode(), notification)
|
||||||
CurrentLessonViewService.startService(applicationContext)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +95,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
"schedule-update" -> {
|
"schedule-update" -> {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
NotificationChannels.SCHEDULE_UPDATE,
|
NotificationChannels.SCHEDULE_UPDATE,
|
||||||
R.drawable.logo,
|
R.drawable.schedule,
|
||||||
getString(R.string.schedule_update_title),
|
getString(R.string.schedule_update_title),
|
||||||
getString(
|
getString(
|
||||||
if (message.data["replaced"] == "true")
|
if (message.data["replaced"] == "true")
|
||||||
@@ -101,24 +106,19 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
NotificationCompat.PRIORITY_DEFAULT,
|
NotificationCompat.PRIORITY_DEFAULT,
|
||||||
message.data["etag"]
|
message.data["etag"]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val constraints = Constraints.Builder()
|
"lessons-start" -> {
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
scope.launch {
|
||||||
.build()
|
CurrentLessonViewService
|
||||||
|
.startService(applicationContext as PolytechnicApplication)
|
||||||
val request = OneTimeWorkRequestBuilder<ScheduleClvAlarm>()
|
}
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(applicationContext)
|
|
||||||
.enqueue(request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"app-update" -> {
|
"app-update" -> {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
NotificationChannels.APP_UPDATE,
|
NotificationChannels.APP_UPDATE,
|
||||||
R.drawable.logo,
|
R.drawable.download,
|
||||||
getString(R.string.app_update_title, message.data["version"]),
|
getString(R.string.app_update_title, message.data["version"]),
|
||||||
getString(R.string.app_update_description),
|
getString(R.string.app_update_description),
|
||||||
NotificationCompat.PRIORITY_DEFAULT,
|
NotificationCompat.PRIORITY_DEFAULT,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import kotlinx.coroutines.launch
|
|||||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||||
import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
|
import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
|
||||||
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
|
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
|
||||||
@@ -101,21 +100,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleAlarm() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val schedule = (applicationContext as PolytechnicApplication)
|
|
||||||
.container
|
|
||||||
.scheduleRepository
|
|
||||||
.getGroup()
|
|
||||||
|
|
||||||
if (schedule is MyResult.Failure)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
(applicationContext as PolytechnicApplication)
|
|
||||||
.scheduleClvService((schedule as MyResult.Success).data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun scheduleLinkUpdate(intervalInMinutes: Long) {
|
fun scheduleLinkUpdate(intervalInMinutes: Long) {
|
||||||
val tag = "schedule-update"
|
val tag = "schedule-update"
|
||||||
@@ -182,7 +166,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
setupFirebaseConfig()
|
setupFirebaseConfig()
|
||||||
|
|
||||||
handleUpdate()
|
handleUpdate()
|
||||||
scheduleAlarm()
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||||
|
|||||||
@@ -1,504 +1,113 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.auth
|
package ru.n08i40k.polytechnic.next.ui.auth
|
||||||
|
|
||||||
import android.content.Context
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideIn
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MenuAnchorType
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.android.volley.AuthFailureError
|
|
||||||
import com.android.volley.ClientError
|
|
||||||
import com.android.volley.TimeoutError
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import ru.n08i40k.polytechnic.next.R
|
|
||||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
|
||||||
import ru.n08i40k.polytechnic.next.model.UserRole.Companion.AcceptableUserRoles
|
|
||||||
import ru.n08i40k.polytechnic.next.network.request.auth.AuthLogin
|
|
||||||
import ru.n08i40k.polytechnic.next.network.request.auth.AuthRegister
|
|
||||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
|
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun LoginForm(
|
|
||||||
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
|
|
||||||
navController: NavHostController = rememberNavController(),
|
|
||||||
scope: CoroutineScope = rememberCoroutineScope(),
|
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
val mutableIsLoading = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var username by remember { mutableStateOf("") }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var visible by mutableVisible
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.login_title),
|
|
||||||
modifier = Modifier.padding(10.dp),
|
|
||||||
style = MaterialTheme.typography.displaySmall,
|
|
||||||
fontWeight = FontWeight.ExtraBold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
|
||||||
|
|
||||||
val mutableUsernameError = remember { mutableStateOf(false) }
|
|
||||||
val mutablePasswordError = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var usernameError by mutableUsernameError
|
|
||||||
var passwordError by mutablePasswordError
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = username,
|
|
||||||
singleLine = true,
|
|
||||||
onValueChange = {
|
|
||||||
username = it
|
|
||||||
usernameError = false
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.username)) },
|
|
||||||
isError = usernameError
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = password,
|
|
||||||
singleLine = true,
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
onValueChange = {
|
|
||||||
passwordError = false
|
|
||||||
password = it
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.password)) },
|
|
||||||
isError = passwordError
|
|
||||||
)
|
|
||||||
|
|
||||||
TextButton(onClick = { visible = false }) {
|
|
||||||
Text(text = stringResource(R.string.not_registered))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
enabled = !mutableIsLoading.value,
|
|
||||||
onClick = {
|
|
||||||
if (username.length < 4) usernameError = true
|
|
||||||
if (password.isEmpty()) passwordError = true
|
|
||||||
|
|
||||||
if (usernameError || passwordError) return@Button
|
|
||||||
|
|
||||||
tryLogin(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
mutableUsernameError,
|
|
||||||
mutablePasswordError,
|
|
||||||
mutableIsLoading,
|
|
||||||
context,
|
|
||||||
snackbarHostState,
|
|
||||||
scope,
|
|
||||||
navController
|
|
||||||
)
|
|
||||||
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.login),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun RegisterForm(
|
|
||||||
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
|
|
||||||
navController: NavHostController = rememberNavController(),
|
|
||||||
scope: CoroutineScope = rememberCoroutineScope(),
|
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
val mutableIsLoading = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var username by remember { mutableStateOf("") }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var group by remember { mutableStateOf("") }
|
|
||||||
val mutableRole = remember { mutableStateOf(UserRole.STUDENT) }
|
|
||||||
|
|
||||||
var visible by mutableVisible
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.register_title),
|
|
||||||
modifier = Modifier.padding(10.dp),
|
|
||||||
style = MaterialTheme.typography.displaySmall,
|
|
||||||
fontWeight = FontWeight.ExtraBold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
|
||||||
|
|
||||||
val mutableUsernameError = remember { mutableStateOf(false) }
|
|
||||||
var usernameError by mutableUsernameError
|
|
||||||
|
|
||||||
var passwordError by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val mutableGroupError = remember { mutableStateOf(false) }
|
|
||||||
var groupError by mutableGroupError
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = username,
|
|
||||||
singleLine = true,
|
|
||||||
onValueChange = {
|
|
||||||
username = it
|
|
||||||
usernameError = false
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.username)) },
|
|
||||||
isError = usernameError
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = password,
|
|
||||||
singleLine = true,
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
onValueChange = {
|
|
||||||
passwordError = false
|
|
||||||
password = it
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.password)) },
|
|
||||||
isError = passwordError
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = group,
|
|
||||||
singleLine = true,
|
|
||||||
onValueChange = {
|
|
||||||
groupError = false
|
|
||||||
group = it
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.group)) },
|
|
||||||
isError = groupError
|
|
||||||
)
|
|
||||||
|
|
||||||
RoleSelector(mutableRole)
|
|
||||||
|
|
||||||
TextButton(onClick = { visible = false }) {
|
|
||||||
Text(text = stringResource(R.string.already_registered))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
enabled = !mutableIsLoading.value,
|
|
||||||
onClick = {
|
|
||||||
if (username.length < 4) usernameError = true
|
|
||||||
if (password.isEmpty()) passwordError = true
|
|
||||||
if (group.isEmpty()) groupError = true
|
|
||||||
|
|
||||||
if (usernameError || passwordError || groupError) return@Button
|
|
||||||
|
|
||||||
tryRegister(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
group,
|
|
||||||
mutableRole.value,
|
|
||||||
mutableUsernameError,
|
|
||||||
mutableGroupError,
|
|
||||||
mutableIsLoading,
|
|
||||||
context,
|
|
||||||
snackbarHostState,
|
|
||||||
scope,
|
|
||||||
navController
|
|
||||||
)
|
|
||||||
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.register),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthForm(
|
fun AuthForm(
|
||||||
mutableIsLogin: MutableState<Boolean> = mutableStateOf(true),
|
appNavController: NavHostController = rememberNavController(),
|
||||||
navController: NavHostController = rememberNavController(),
|
onPendingSnackbar: (String) -> Unit = {},
|
||||||
scope: CoroutineScope = rememberCoroutineScope(),
|
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
|
||||||
) {
|
) {
|
||||||
var isLogin by mutableIsLogin
|
val navController = rememberNavController()
|
||||||
|
|
||||||
val mutableVisible = remember { mutableStateOf(true) }
|
val modifier = Modifier.fillMaxSize()
|
||||||
var visible by mutableVisible
|
|
||||||
|
|
||||||
val animatedAlpha by animateFloatAsState(
|
NavHost(
|
||||||
targetValue = if (visible) 1.0f else 0f, label = "alpha"
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
navController = navController,
|
||||||
|
startDestination = "sign-in",
|
||||||
|
enterTransition = {
|
||||||
|
slideIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
400,
|
||||||
|
delayMillis = 250,
|
||||||
|
easing = LinearOutSlowInEasing
|
||||||
|
)
|
||||||
|
) { fullSize -> IntOffset(0, fullSize.height / 16) } + fadeIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
400,
|
||||||
|
delayMillis = 250,
|
||||||
|
easing = LinearOutSlowInEasing
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = animatedAlpha
|
|
||||||
if (alpha == 0F) {
|
|
||||||
if (!visible) isLogin = isLogin.not()
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
if (isLogin)
|
|
||||||
LoginForm(mutableVisible, navController, scope, snackbarHostState)
|
|
||||||
else
|
|
||||||
RegisterForm(mutableVisible, navController, scope, snackbarHostState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun RoleSelector(mutableRole: MutableState<UserRole> = mutableStateOf(UserRole.STUDENT)) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var role by mutableRole
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.wrapContentSize()
|
|
||||||
) {
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = !expanded }
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
label = { Text(stringResource(R.string.role)) },
|
|
||||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
|
||||||
value = stringResource(role.stringId),
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = role.icon,
|
|
||||||
contentDescription = "role icon"
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onValueChange = {},
|
exitTransition = {
|
||||||
readOnly = true,
|
fadeOut(
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
animationSpec = tween(
|
||||||
|
250,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
composable("sign-in") {
|
||||||
|
Row(
|
||||||
|
modifier,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
|
||||||
|
LoginForm(appNavController, navController, onPendingSnackbar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("sign-up") {
|
||||||
|
Row(
|
||||||
|
modifier,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
|
||||||
|
RegisterForm(appNavController, navController, onPendingSnackbar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }) {
|
|
||||||
AcceptableUserRoles.forEach {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(it.stringId)) },
|
|
||||||
onClick = {
|
|
||||||
role = it
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryLogin(
|
|
||||||
// data
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
|
|
||||||
// errors
|
|
||||||
mutableUsernameError: MutableState<Boolean>,
|
|
||||||
mutablePasswordError: MutableState<Boolean>,
|
|
||||||
|
|
||||||
// additional
|
|
||||||
mutableIsLoading: MutableState<Boolean>,
|
|
||||||
context: Context,
|
|
||||||
snackbarHostState: SnackbarHostState,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
navController: NavHostController
|
|
||||||
) {
|
|
||||||
var isLoading by mutableIsLoading
|
|
||||||
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
AuthLogin(AuthLogin.RequestDto(username, password), context, {
|
|
||||||
runBlocking {
|
|
||||||
context.settingsDataStore.updateData { currentSettings ->
|
|
||||||
currentSettings
|
|
||||||
.toBuilder()
|
|
||||||
.setUserId(it.id)
|
|
||||||
.setAccessToken(it.accessToken)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileMe(context, {
|
|
||||||
scope.launch { snackbarHostState.showSnackbar("Cool!") }
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
context.settingsDataStore.updateData { currentSettings ->
|
|
||||||
currentSettings
|
|
||||||
.toBuilder()
|
|
||||||
.setGroup(it.group)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navController.navigate("main")
|
|
||||||
}, null).send()
|
|
||||||
}, {
|
|
||||||
isLoading = false
|
|
||||||
|
|
||||||
if (it is TimeoutError) {
|
|
||||||
scope.launch { snackbarHostState.showSnackbar("Request timed out!") }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it is ClientError && it.networkResponse.statusCode == 400) scope.launch {
|
|
||||||
snackbarHostState.showSnackbar("Request schema not identical!")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it is AuthFailureError) scope.launch {
|
|
||||||
mutableUsernameError.value = true
|
|
||||||
mutablePasswordError.value = true
|
|
||||||
snackbarHostState.showSnackbar("Invalid credentials!")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
it.printStackTrace()
|
|
||||||
}).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tryRegister(
|
|
||||||
// data
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
group: String,
|
|
||||||
role: UserRole,
|
|
||||||
|
|
||||||
// errors
|
|
||||||
mutableUsernameError: MutableState<Boolean>,
|
|
||||||
mutableGroupError: MutableState<Boolean>,
|
|
||||||
|
|
||||||
// additional
|
|
||||||
mutableIsLoading: MutableState<Boolean>,
|
|
||||||
context: Context,
|
|
||||||
snackbarHostState: SnackbarHostState,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
navController: NavHostController
|
|
||||||
) {
|
|
||||||
var isLoading by mutableIsLoading
|
|
||||||
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
AuthRegister(
|
|
||||||
AuthRegister.RequestDto(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
group,
|
|
||||||
role
|
|
||||||
), context, {
|
|
||||||
scope.launch { snackbarHostState.showSnackbar("Cool!") }
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
context.settingsDataStore.updateData { currentSettings ->
|
|
||||||
currentSettings.toBuilder().setUserId(it.id)
|
|
||||||
.setAccessToken(it.accessToken).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
context.settingsDataStore.updateData { currentSettings ->
|
|
||||||
currentSettings
|
|
||||||
.toBuilder()
|
|
||||||
.setGroup(group)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navController.navigate("main")
|
|
||||||
}, {
|
|
||||||
isLoading = false
|
|
||||||
|
|
||||||
if (it is TimeoutError) {
|
|
||||||
scope.launch { snackbarHostState.showSnackbar("Request timed out!") }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it is ClientError) scope.launch {
|
|
||||||
val statusCode = it.networkResponse.statusCode
|
|
||||||
|
|
||||||
when (statusCode) {
|
|
||||||
400 -> snackbarHostState.showSnackbar("Request schema not identical!")
|
|
||||||
409 -> {
|
|
||||||
mutableUsernameError.value = true
|
|
||||||
snackbarHostState.showSnackbar("User already exists!")
|
|
||||||
}
|
|
||||||
|
|
||||||
404 -> {
|
|
||||||
mutableGroupError.value = true
|
|
||||||
snackbarHostState.showSnackbar("Group doesn't exists!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it is AuthFailureError) scope.launch {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
"Invalid credentials!"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
it.printStackTrace()
|
|
||||||
}).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
fun AuthScreen(appNavController: NavHostController = rememberNavController()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -506,14 +115,20 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
|||||||
context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
|
context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken.isNotEmpty()) navController.navigate("main")
|
if (accessToken.isNotEmpty()) {
|
||||||
|
appNavController.navigate("main") {
|
||||||
|
popUpTo("auth") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val mutableIsLogin = remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val onPendingSnackbar: (String) -> Unit = {
|
||||||
|
scope.launch { snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Long) }
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
|
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||||
@@ -525,14 +140,10 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
|||||||
horizontalArrangement = Arrangement.SpaceAround,
|
horizontalArrangement = Arrangement.SpaceAround,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Card {
|
|
||||||
AuthForm(
|
AuthForm(
|
||||||
mutableIsLogin,
|
appNavController,
|
||||||
navController,
|
onPendingSnackbar
|
||||||
scope,
|
|
||||||
snackbarHostState
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.auth
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun LoginForm(
|
||||||
|
appNavController: NavHostController = rememberNavController(),
|
||||||
|
navController: NavHostController = rememberNavController(),
|
||||||
|
onPendingSnackbar: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
var loading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
var usernameError by remember { mutableStateOf(false) }
|
||||||
|
var passwordError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val onClick = fun() {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
|
||||||
|
if (username.length < 4) usernameError = true
|
||||||
|
if (password.isEmpty()) passwordError = true
|
||||||
|
|
||||||
|
if (usernameError || passwordError) return
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
trySignIn(
|
||||||
|
context,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
{
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
val stringRes = when (it) {
|
||||||
|
SignInError.INVALID_CREDENTIALS -> {
|
||||||
|
usernameError = true
|
||||||
|
passwordError = true
|
||||||
|
|
||||||
|
R.string.invalid_credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
SignInError.TIMED_OUT -> R.string.timed_out
|
||||||
|
SignInError.NO_CONNECTION -> R.string.no_connection
|
||||||
|
SignInError.APPLICATION_TOO_OLD -> R.string.app_too_old
|
||||||
|
SignInError.UNKNOWN -> R.string.unknown_error
|
||||||
|
}
|
||||||
|
|
||||||
|
onPendingSnackbar(context.getString(stringRes))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
appNavController.navigate("main") {
|
||||||
|
popUpTo("auth") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sign_in_title),
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
style = MaterialTheme.typography.displaySmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = username,
|
||||||
|
singleLine = true,
|
||||||
|
onValueChange = {
|
||||||
|
username = it
|
||||||
|
usernameError = false
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.username)) },
|
||||||
|
isError = usernameError
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
onValueChange = {
|
||||||
|
passwordError = false
|
||||||
|
password = it
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.password)) },
|
||||||
|
isError = passwordError
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = { navController.navigate("sign-up") }) {
|
||||||
|
Text(text = stringResource(R.string.not_registered))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
enabled = !loading && !(usernameError || passwordError),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.proceed),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.auth
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.widgets.RoleSelector
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun RegisterForm(
|
||||||
|
appNavController: NavHostController = rememberNavController(),
|
||||||
|
navController: NavHostController = rememberNavController(),
|
||||||
|
onPendingSnackbar: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
var loading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var group by remember { mutableStateOf<String?>(null) }
|
||||||
|
var role by remember { mutableStateOf(UserRole.STUDENT) }
|
||||||
|
|
||||||
|
var usernameError by remember { mutableStateOf(false) }
|
||||||
|
var passwordError by remember { mutableStateOf(false) }
|
||||||
|
var groupError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val onClick = fun() {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
|
||||||
|
if (username.length < 4) usernameError = true
|
||||||
|
if (password.isEmpty()) passwordError = true
|
||||||
|
|
||||||
|
if (usernameError || passwordError || groupError) return
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
trySignUp(
|
||||||
|
context,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
group!!,
|
||||||
|
role,
|
||||||
|
{
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
val stringRes = when (it) {
|
||||||
|
SignUpError.UNKNOWN -> R.string.unknown_error
|
||||||
|
SignUpError.ALREADY_EXISTS -> R.string.already_exists
|
||||||
|
SignUpError.APPLICATION_TOO_OLD -> R.string.app_too_old
|
||||||
|
SignUpError.TIMED_OUT -> R.string.timed_out
|
||||||
|
SignUpError.NO_CONNECTION -> R.string.no_connection
|
||||||
|
SignUpError.GROUP_DOES_NOT_EXISTS -> R.string.group_does_not_exists
|
||||||
|
}
|
||||||
|
|
||||||
|
onPendingSnackbar(context.getString(stringRes))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
appNavController.navigate("main") {
|
||||||
|
popUpTo("auth") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sign_up_title),
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
style = MaterialTheme.typography.displaySmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = username,
|
||||||
|
singleLine = true,
|
||||||
|
onValueChange = {
|
||||||
|
username = it
|
||||||
|
usernameError = false
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.username)) },
|
||||||
|
isError = usernameError,
|
||||||
|
readOnly = loading
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
onValueChange = {
|
||||||
|
passwordError = false
|
||||||
|
password = it
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.password)) },
|
||||||
|
isError = passwordError,
|
||||||
|
readOnly = loading
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
GroupSelector(
|
||||||
|
value = group,
|
||||||
|
isError = groupError,
|
||||||
|
readOnly = loading
|
||||||
|
) {
|
||||||
|
groupError = false
|
||||||
|
group = it
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
RoleSelector(
|
||||||
|
value = role,
|
||||||
|
isError = false,
|
||||||
|
readOnly = loading
|
||||||
|
) { role = it }
|
||||||
|
|
||||||
|
TextButton(onClick = { navController.navigate("sign-in") }) {
|
||||||
|
Text(text = stringResource(R.string.already_registered))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
enabled = !loading && group != null && !(usernameError || passwordError || groupError),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.proceed),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.auth
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.volley.AuthFailureError
|
||||||
|
import com.android.volley.ClientError
|
||||||
|
import com.android.volley.NoConnectionError
|
||||||
|
import com.android.volley.TimeoutError
|
||||||
|
import com.google.firebase.logger.Logger
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn
|
||||||
|
import ru.n08i40k.polytechnic.next.network.unwrapException
|
||||||
|
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
internal enum class SignInError {
|
||||||
|
INVALID_CREDENTIALS,
|
||||||
|
TIMED_OUT,
|
||||||
|
NO_CONNECTION,
|
||||||
|
APPLICATION_TOO_OLD,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun trySignIn(
|
||||||
|
context: Context,
|
||||||
|
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
|
||||||
|
onError: (SignInError) -> Unit,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
AuthSignIn(AuthSignIn.RequestDto(username, password), context, {
|
||||||
|
runBlocking {
|
||||||
|
context.settingsDataStore.updateData { currentSettings ->
|
||||||
|
currentSettings
|
||||||
|
.toBuilder()
|
||||||
|
.setUserId(it.id)
|
||||||
|
.setAccessToken(it.accessToken)
|
||||||
|
.setGroup(it.group)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess()
|
||||||
|
}, {
|
||||||
|
val error = when (val exception = unwrapException(it)) {
|
||||||
|
is TimeoutException -> SignInError.TIMED_OUT
|
||||||
|
is TimeoutError -> SignInError.TIMED_OUT
|
||||||
|
is NoConnectionError -> SignInError.NO_CONNECTION
|
||||||
|
is AuthFailureError -> SignInError.INVALID_CREDENTIALS
|
||||||
|
is ClientError -> {
|
||||||
|
if (exception.networkResponse.statusCode == 400)
|
||||||
|
SignInError.APPLICATION_TOO_OLD
|
||||||
|
else
|
||||||
|
SignInError.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> SignInError.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == SignInError.UNKNOWN) {
|
||||||
|
Logger.getLogger("tryLogin")
|
||||||
|
.error("Unknown exception while trying to login!", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error)
|
||||||
|
}).send()
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.auth
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.volley.ClientError
|
||||||
|
import com.android.volley.NoConnectionError
|
||||||
|
import com.android.volley.TimeoutError
|
||||||
|
import com.google.firebase.logger.Logger
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
|
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp
|
||||||
|
import ru.n08i40k.polytechnic.next.network.unwrapException
|
||||||
|
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
internal enum class SignUpError {
|
||||||
|
ALREADY_EXISTS,
|
||||||
|
GROUP_DOES_NOT_EXISTS,
|
||||||
|
TIMED_OUT,
|
||||||
|
NO_CONNECTION,
|
||||||
|
APPLICATION_TOO_OLD,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun trySignUp(
|
||||||
|
context: Context,
|
||||||
|
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
group: String,
|
||||||
|
role: UserRole,
|
||||||
|
|
||||||
|
onError: (SignUpError) -> Unit,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
AuthSignUp(
|
||||||
|
AuthSignUp.RequestDto(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
group,
|
||||||
|
role
|
||||||
|
), context, {
|
||||||
|
runBlocking {
|
||||||
|
context.settingsDataStore.updateData { currentSettings ->
|
||||||
|
currentSettings
|
||||||
|
.toBuilder()
|
||||||
|
.setUserId(it.id)
|
||||||
|
.setAccessToken(it.accessToken)
|
||||||
|
.setGroup(group)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess()
|
||||||
|
}, {
|
||||||
|
val error = when (val exception = unwrapException(it)) {
|
||||||
|
is TimeoutException -> SignUpError.TIMED_OUT
|
||||||
|
is NoConnectionError -> SignUpError.NO_CONNECTION
|
||||||
|
is TimeoutError -> SignUpError.UNKNOWN
|
||||||
|
is ClientError -> {
|
||||||
|
when (exception.networkResponse.statusCode) {
|
||||||
|
400 -> SignUpError.APPLICATION_TOO_OLD
|
||||||
|
404 -> SignUpError.GROUP_DOES_NOT_EXISTS
|
||||||
|
409 -> SignUpError.ALREADY_EXISTS
|
||||||
|
else -> SignUpError.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> SignUpError.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == SignUpError.UNKNOWN) {
|
||||||
|
Logger.getLogger("tryRegister")
|
||||||
|
.error("Unknown exception while trying to register!", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error)
|
||||||
|
}).send()
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||||
|
|
||||||
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
val FilledGroup.Download: ImageVector
|
val FilledGroup.Download: ImageVector
|
||||||
get() {
|
get() {
|
||||||
if (_download != null) {
|
if (_download != null) {
|
||||||
return _download!!
|
return _download!!
|
||||||
}
|
}
|
||||||
_download = Builder(
|
_download = Builder(
|
||||||
name = "Download", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
name = "Download", defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||||
viewportWidth = 24.0f, viewportHeight = 24.0f
|
viewportWidth = 24.0f, viewportHeight = 24.0f
|
||||||
).apply {
|
).apply {
|
||||||
path(
|
path(
|
||||||
@@ -55,5 +56,4 @@ val FilledGroup.Download: ImageVector
|
|||||||
return _download!!
|
return _download!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ObjectPropertyName")
|
|
||||||
private var _download: ImageVector? = null
|
private var _download: ImageVector? = null
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||||
|
|
||||||
|
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||||
|
|
||||||
|
val FilledGroup.Error: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_error != null) {
|
||||||
|
return _error!!
|
||||||
|
}
|
||||||
|
_error = Builder(
|
||||||
|
name = "Error", defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24.0f, viewportHeight = 24.0f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = EvenOdd
|
||||||
|
) {
|
||||||
|
moveTo(1.25f, 8.0f)
|
||||||
|
curveTo(1.25f, 4.2721f, 4.2721f, 1.25f, 8.0f, 1.25f)
|
||||||
|
lineTo(16.0f, 1.25f)
|
||||||
|
curveTo(19.7279f, 1.25f, 22.75f, 4.2721f, 22.75f, 8.0f)
|
||||||
|
lineTo(22.75f, 16.0f)
|
||||||
|
curveTo(22.75f, 19.7279f, 19.7279f, 22.75f, 16.0f, 22.75f)
|
||||||
|
lineTo(8.0f, 22.75f)
|
||||||
|
curveTo(4.2721f, 22.75f, 1.25f, 19.7279f, 1.25f, 16.0f)
|
||||||
|
lineTo(1.25f, 8.0f)
|
||||||
|
close()
|
||||||
|
moveTo(8.4697f, 8.4697f)
|
||||||
|
curveTo(8.7626f, 8.1768f, 9.2374f, 8.1768f, 9.5303f, 8.4697f)
|
||||||
|
lineTo(12.0f, 10.9393f)
|
||||||
|
lineTo(14.4697f, 8.4697f)
|
||||||
|
curveTo(14.7626f, 8.1768f, 15.2374f, 8.1768f, 15.5303f, 8.4697f)
|
||||||
|
curveTo(15.8232f, 8.7626f, 15.8232f, 9.2374f, 15.5303f, 9.5303f)
|
||||||
|
lineTo(13.0606f, 12.0f)
|
||||||
|
lineTo(15.5303f, 14.4697f)
|
||||||
|
curveTo(15.8232f, 14.7626f, 15.8232f, 15.2374f, 15.5303f, 15.5303f)
|
||||||
|
curveTo(15.2374f, 15.8232f, 14.7625f, 15.8232f, 14.4696f, 15.5303f)
|
||||||
|
lineTo(12.0f, 13.0606f)
|
||||||
|
lineTo(9.5303f, 15.5303f)
|
||||||
|
curveTo(9.2374f, 15.8232f, 8.7626f, 15.8232f, 8.4697f, 15.5303f)
|
||||||
|
curveTo(8.1768f, 15.2374f, 8.1768f, 14.7625f, 8.4697f, 14.4696f)
|
||||||
|
lineTo(10.9393f, 12.0f)
|
||||||
|
lineTo(8.4697f, 9.5303f)
|
||||||
|
curveTo(8.1768f, 9.2374f, 8.1768f, 8.7626f, 8.4697f, 8.4697f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _error!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _error: ImageVector? = null
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||||
|
|
||||||
|
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||||
|
|
||||||
|
val FilledGroup.Info: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_info != null) {
|
||||||
|
return _info!!
|
||||||
|
}
|
||||||
|
_info = Builder(
|
||||||
|
name = "Info", defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24.0f, viewportHeight = 24.0f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero
|
||||||
|
) {
|
||||||
|
moveTo(12.0f, 2.0f)
|
||||||
|
arcTo(10.0f, 10.0f, 0.0f, true, false, 22.0f, 12.0f)
|
||||||
|
arcTo(10.0f, 10.0f, 0.0f, false, false, 12.0f, 2.0f)
|
||||||
|
close()
|
||||||
|
moveTo(13.0f, 17.0f)
|
||||||
|
arcToRelative(1.0f, 1.0f, 0.0f, false, true, -2.0f, 0.0f)
|
||||||
|
lineTo(11.0f, 11.0f)
|
||||||
|
arcToRelative(1.0f, 1.0f, 0.0f, false, true, 2.0f, 0.0f)
|
||||||
|
close()
|
||||||
|
moveTo(12.0f, 8.0f)
|
||||||
|
arcToRelative(1.5f, 1.5f, 0.0f, true, true, 1.5f, -1.5f)
|
||||||
|
arcTo(1.5f, 1.5f, 0.0f, false, true, 12.0f, 8.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _info!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _info: ImageVector? = null
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||||
|
|
||||||
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
val FilledGroup.Telegram: ImageVector
|
val FilledGroup.Telegram: ImageVector
|
||||||
get() {
|
get() {
|
||||||
if (_telegram != null) {
|
if (_telegram != null) {
|
||||||
return _telegram!!
|
return _telegram!!
|
||||||
}
|
}
|
||||||
_telegram = Builder(
|
_telegram = Builder(
|
||||||
name = "Telegram", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
name = "Telegram", defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||||
viewportWidth = 24.0f, viewportHeight = 24.0f
|
viewportWidth = 24.0f, viewportHeight = 24.0f
|
||||||
).apply {
|
).apply {
|
||||||
path(
|
path(
|
||||||
@@ -49,5 +50,4 @@ val FilledGroup.Telegram: ImageVector
|
|||||||
return _telegram!!
|
return _telegram!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ObjectPropertyName")
|
|
||||||
private var _telegram: ImageVector? = null
|
private var _telegram: ImageVector? = null
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||||
|
|
||||||
|
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||||
|
|
||||||
|
val FilledGroup.Warning: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_warning != null) {
|
||||||
|
return _warning!!
|
||||||
|
}
|
||||||
|
_warning = Builder(
|
||||||
|
name = "Warning", defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 512.0f, viewportHeight = 512.0f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color(0xFF000000)), stroke = SolidColor(Color(0x00000000)),
|
||||||
|
strokeLineWidth = 1.0f, strokeLineCap = Butt, strokeLineJoin = Miter,
|
||||||
|
strokeLineMiter = 4.0f, pathFillType = EvenOdd
|
||||||
|
) {
|
||||||
|
moveTo(278.313f, 48.296f)
|
||||||
|
curveTo(284.928f, 52.075f, 290.41f, 57.557f, 294.189f, 64.172f)
|
||||||
|
lineTo(476.667f, 383.508f)
|
||||||
|
curveTo(488.358f, 403.967f, 481.25f, 430.03f, 460.791f, 441.722f)
|
||||||
|
curveTo(454.344f, 445.405f, 447.047f, 447.343f, 439.622f, 447.343f)
|
||||||
|
lineTo(74.667f, 447.343f)
|
||||||
|
curveTo(51.103f, 447.343f, 32.0f, 428.241f, 32.0f, 404.677f)
|
||||||
|
curveTo(32.0f, 397.251f, 33.938f, 389.955f, 37.622f, 383.508f)
|
||||||
|
lineTo(220.099f, 64.172f)
|
||||||
|
curveTo(231.79f, 43.713f, 257.854f, 36.605f, 278.313f, 48.296f)
|
||||||
|
close()
|
||||||
|
moveTo(256.0f, 314.667f)
|
||||||
|
curveTo(240.762f, 314.667f, 229.333f, 325.931f, 229.333f, 340.949f)
|
||||||
|
curveTo(229.333f, 356.651f, 240.416f, 367.915f, 256.0f, 367.915f)
|
||||||
|
curveTo(271.238f, 367.915f, 282.667f, 356.651f, 282.667f, 341.291f)
|
||||||
|
curveTo(282.667f, 325.931f, 271.238f, 314.667f, 256.0f, 314.667f)
|
||||||
|
close()
|
||||||
|
moveTo(277.333f, 149.333f)
|
||||||
|
lineTo(234.667f, 149.333f)
|
||||||
|
lineTo(234.667f, 277.333f)
|
||||||
|
lineTo(277.333f, 277.333f)
|
||||||
|
lineTo(277.333f, 149.333f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _warning!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _warning: ImageVector? = null
|
||||||
@@ -116,7 +116,7 @@ private fun NavHostContainer(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
builder = {
|
) {
|
||||||
composable("profile") {
|
composable("profile") {
|
||||||
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
|
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ private fun NavHostContainer(
|
|||||||
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
|
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLink(context: Context, link: String) {
|
private fun openLink(context: Context, link: String) {
|
||||||
@@ -260,7 +260,11 @@ fun MainScreen(
|
|||||||
viewModel(
|
viewModel(
|
||||||
factory = ProfileViewModel.provideFactory(
|
factory = ProfileViewModel.provideFactory(
|
||||||
profileRepository = mainViewModel.appContainer.profileRepository,
|
profileRepository = mainViewModel.appContainer.profileRepository,
|
||||||
onUnauthorized = { appNavController.navigate("auth") })
|
onUnauthorized = {
|
||||||
|
appNavController.navigate("auth") {
|
||||||
|
popUpTo("main") { inclusive = true }
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
LocalContext.current.profileViewModel = profileViewModel
|
LocalContext.current.profileViewModel = profileViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.main.profile
|
package ru.n08i40k.polytechnic.next.ui.main.profile
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Email
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MenuAnchorType
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -36,7 +24,7 @@ import ru.n08i40k.polytechnic.next.R
|
|||||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
import ru.n08i40k.polytechnic.next.model.Profile
|
||||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
|
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
|
import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
|
||||||
|
|
||||||
private enum class ChangeGroupError {
|
private enum class ChangeGroupError {
|
||||||
NOT_EXISTS
|
NOT_EXISTS
|
||||||
@@ -57,80 +45,6 @@ private fun tryChangeGroup(
|
|||||||
}).send()
|
}).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getGroups(context: Context): ArrayList<String> {
|
|
||||||
val groupPlaceholder = stringResource(R.string.loading)
|
|
||||||
|
|
||||||
val groups = remember { arrayListOf(groupPlaceholder) }
|
|
||||||
|
|
||||||
LaunchedEffect(groups) {
|
|
||||||
ScheduleGetGroupNames(context, {
|
|
||||||
groups.clear()
|
|
||||||
groups.addAll(it.names)
|
|
||||||
}, {
|
|
||||||
throw it
|
|
||||||
}).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun GroupSelector(
|
|
||||||
value: String = "ИС-214/24",
|
|
||||||
onValueChange: (String) -> Unit = {},
|
|
||||||
isError: Boolean = false,
|
|
||||||
readOnly: Boolean = false,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.wrapContentSize()
|
|
||||||
) {
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = {
|
|
||||||
expanded = !readOnly && !expanded
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
label = { Text(stringResource(R.string.group)) },
|
|
||||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
|
||||||
value = value,
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Email,
|
|
||||||
contentDescription = "group"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onValueChange = {},
|
|
||||||
isError = isError,
|
|
||||||
readOnly = true,
|
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
|
||||||
)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val groups = getGroups(context)
|
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }) {
|
|
||||||
groups.forEach {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(it) },
|
|
||||||
onClick = {
|
|
||||||
onValueChange(it)
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ChangeGroupDialog(
|
internal fun ChangeGroupDialog(
|
||||||
@@ -141,7 +55,7 @@ internal fun ChangeGroupDialog(
|
|||||||
) {
|
) {
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card {
|
Card {
|
||||||
var group by remember { mutableStateOf("ИС-214/23") }
|
var group by remember { mutableStateOf<String?>(profile.group) }
|
||||||
var groupError by remember { mutableStateOf(false) }
|
var groupError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var processing by remember { mutableStateOf(false) }
|
var processing by remember { mutableStateOf(false) }
|
||||||
@@ -165,7 +79,7 @@ internal fun ChangeGroupDialog(
|
|||||||
|
|
||||||
tryChangeGroup(
|
tryChangeGroup(
|
||||||
context = context,
|
context = context,
|
||||||
group = group,
|
group = group!!,
|
||||||
onError = {
|
onError = {
|
||||||
when (it) {
|
when (it) {
|
||||||
ChangeGroupError.NOT_EXISTS -> {
|
ChangeGroupError.NOT_EXISTS -> {
|
||||||
@@ -178,7 +92,7 @@ internal fun ChangeGroupDialog(
|
|||||||
onSuccess = onChange
|
onSuccess = onChange
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
enabled = !(groupError || processing)
|
enabled = !(groupError || processing) && group != null
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.change_group))
|
Text(stringResource(R.string.change_group))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
import ru.n08i40k.polytechnic.next.model.Profile
|
||||||
@@ -187,6 +188,10 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
|
|||||||
context.settingsDataStore.updateData {
|
context.settingsDataStore.updateData {
|
||||||
it.toBuilder().setGroup(group).build()
|
it.toBuilder().setGroup(group).build()
|
||||||
}
|
}
|
||||||
|
(context.applicationContext as PolytechnicApplication)
|
||||||
|
.container
|
||||||
|
.networkCacheRepository
|
||||||
|
.clear()
|
||||||
}
|
}
|
||||||
context.profileViewModel!!.refreshProfile {
|
context.profileViewModel!!.refreshProfile {
|
||||||
scheduleViewModel.refreshGroup()
|
scheduleViewModel.refreshGroup()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
|
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
|
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import ru.n08i40k.polytechnic.next.R
|
|||||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
|
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState
|
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
|
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -33,23 +33,15 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.Day
|
import ru.n08i40k.polytechnic.next.model.Day
|
||||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
|
||||||
import ru.n08i40k.polytechnic.next.model.LessonType
|
import ru.n08i40k.polytechnic.next.model.LessonType
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
private fun getCurrentMinutes(): Int {
|
|
||||||
return Calendar.getInstance()
|
|
||||||
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
|
|
||||||
.get(Calendar.MINUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getMinutes(): Flow<Int> {
|
private fun getCurrentLessonIdx(day: Day?): Flow<Int> {
|
||||||
val value by remember {
|
val value by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
flow {
|
flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
emit(getCurrentMinutes())
|
emit(day?.currentIdx ?: -1)
|
||||||
delay(5_000)
|
delay(5_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,22 +51,6 @@ private fun getMinutes(): Flow<Int> {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
|
|
||||||
val currentMinutes by getMinutes().collectAsStateWithLifecycle(0)
|
|
||||||
|
|
||||||
val filteredLessons = lessons
|
|
||||||
.filterNotNull()
|
|
||||||
.filter {
|
|
||||||
it.time.start <= currentMinutes && it.time.end >= currentMinutes
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredLessons.isEmpty())
|
|
||||||
return -1
|
|
||||||
|
|
||||||
return lessons.indexOf(filteredLessons[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DayCard(
|
fun DayCard(
|
||||||
@@ -99,8 +75,8 @@ fun DayCard(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (current) MaterialTheme.colorScheme.inverseSurface
|
if (current) MaterialTheme.colorScheme.primaryContainer
|
||||||
else MaterialTheme.colorScheme.surface
|
else MaterialTheme.colorScheme.secondaryContainer
|
||||||
),
|
),
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
|
||||||
) {
|
) {
|
||||||
@@ -109,14 +85,10 @@ fun DayCard(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
text = stringResource(R.string.day_null),
|
text = stringResource(R.string.day_null)
|
||||||
color =
|
|
||||||
if (current) MaterialTheme.colorScheme.inverseOnSurface
|
|
||||||
else MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
return@Card
|
return@Card
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -124,7 +96,8 @@ fun DayCard(
|
|||||||
text = day.name,
|
text = day.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
val currentLessonIdx = calculateCurrentLessonIdx(day.lessons)
|
val currentLessonIdx by getCurrentLessonIdx(if (current) day else null)
|
||||||
|
.collectAsStateWithLifecycle(0)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -1,21 +1,50 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||||
import ru.n08i40k.polytechnic.next.model.Group
|
import ru.n08i40k.polytechnic.next.model.Group
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.temporal.WeekFields
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.logging.Level
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
private fun isCurrentWeek(group: Group): Boolean {
|
||||||
|
if (group.days.size == 0 || group.days[0] == null)
|
||||||
|
return true
|
||||||
|
|
||||||
|
val dateString = group.days[0]!!.name
|
||||||
|
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale("ru"))
|
||||||
|
val datePart = dateString.split(" ").getOrNull(1) ?: return true
|
||||||
|
|
||||||
|
val date = LocalDate.parse(datePart, formatter)
|
||||||
|
val currentDate = LocalDate.now()
|
||||||
|
|
||||||
|
val weekField = WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear()
|
||||||
|
val currentWeek = currentDate.get(weekField)
|
||||||
|
val dateWeek = date.get(weekField)
|
||||||
|
|
||||||
|
return dateWeek >= currentWeek
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
||||||
@@ -23,14 +52,20 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
|||||||
val calendarDay = currentDay
|
val calendarDay = currentDay
|
||||||
.coerceAtLeast(0)
|
.coerceAtLeast(0)
|
||||||
.coerceAtMost(group.days.size - 1)
|
.coerceAtMost(group.days.size - 1)
|
||||||
|
|
||||||
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size })
|
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size })
|
||||||
|
|
||||||
|
Column {
|
||||||
|
if (!isCurrentWeek(group)) {
|
||||||
|
NotificationCard(
|
||||||
|
level = Level.WARNING,
|
||||||
|
title = stringResource(R.string.outdated_schedule)
|
||||||
|
)
|
||||||
|
}
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(horizontal = 20.dp),
|
contentPadding = PaddingValues(horizontal = 20.dp),
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
modifier = Modifier.height(600.dp)
|
modifier = Modifier.height(600.dp).padding(top = 5.dp)
|
||||||
) { page ->
|
) { page ->
|
||||||
DayCard(
|
DayCard(
|
||||||
modifier = Modifier.graphicsLayer {
|
modifier = Modifier.graphicsLayer {
|
||||||
@@ -50,4 +85,5 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
|||||||
current = currentDay == page
|
current = currentDay == page
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
|
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
import ru.n08i40k.polytechnic.next.UpdateDates
|
||||||
import ru.n08i40k.polytechnic.next.ui.ExpandableCard
|
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCard
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCardTitle
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -49,7 +50,7 @@ fun UpdateInfo(
|
|||||||
ExpandableCard(
|
ExpandableCard(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = { expanded = !expanded },
|
onExpandedChange = { expanded = !expanded },
|
||||||
title = stringResource(R.string.update_info_header)
|
title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.theme
|
package ru.n08i40k.polytechnic.next.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val primaryLight = Color(0xFF4C662B)
|
val primaryLight = Color(0xFF4C662B)
|
||||||
@@ -218,9 +217,33 @@ val surfaceContainerDarkHighContrast = Color(0xFF1E201A)
|
|||||||
val surfaceContainerHighDarkHighContrast = Color(0xFF282B24)
|
val surfaceContainerHighDarkHighContrast = Color(0xFF282B24)
|
||||||
val surfaceContainerHighestDarkHighContrast = Color(0xFF33362E)
|
val surfaceContainerHighestDarkHighContrast = Color(0xFF33362E)
|
||||||
|
|
||||||
|
val warningLight = Color(0xFF7B580C)
|
||||||
|
val onWarningLight = Color(0xFFFFFFFF)
|
||||||
|
val warningContainerLight = Color(0xFFFFDEA8)
|
||||||
|
val onWarningContainerLight = Color(0xFF271900)
|
||||||
|
|
||||||
|
val warningLightMediumContrast = Color(0xFF593E00)
|
||||||
|
val onWarningLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val warningContainerLightMediumContrast = Color(0xFF946E24)
|
||||||
|
val onWarningContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
val warningLightHighContrast = Color(0xFF2F1F00)
|
||||||
|
val onWarningLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val warningContainerLightHighContrast = Color(0xFF593E00)
|
||||||
|
val onWarningContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
val warningDark = Color(0xFFEEBF6D)
|
||||||
|
val onWarningDark = Color(0xFF422D00)
|
||||||
|
val warningContainerDark = Color(0xFF5E4200)
|
||||||
|
val onWarningContainerDark = Color(0xFFFFDEA8)
|
||||||
|
|
||||||
|
val warningDarkMediumContrast = Color(0xFFF2C470)
|
||||||
|
val onWarningDarkMediumContrast = Color(0xFF201400)
|
||||||
|
val warningContainerDarkMediumContrast = Color(0xFFB38A3D)
|
||||||
|
val onWarningContainerDarkMediumContrast = Color(0xFF000000)
|
||||||
|
|
||||||
|
val warningDarkHighContrast = Color(0xFFFFFAF7)
|
||||||
|
val onWarningDarkHighContrast = Color(0xFF000000)
|
||||||
|
val warningContainerDarkHighContrast = Color(0xFFF2C470)
|
||||||
|
val onWarningContainerDarkHighContrast = Color(0xFF000000)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ExtendedColorScheme(
|
||||||
|
val warning: ColorFamily,
|
||||||
|
)
|
||||||
|
|
||||||
private val lightScheme = lightColorScheme(
|
private val lightScheme = lightColorScheme(
|
||||||
primary = primaryLight,
|
primary = primaryLight,
|
||||||
onPrimary = onPrimaryLight,
|
onPrimary = onPrimaryLight,
|
||||||
@@ -242,6 +247,60 @@ private val highContrastDarkColorScheme = darkColorScheme(
|
|||||||
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val extendedLight = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningLight,
|
||||||
|
onWarningLight,
|
||||||
|
warningContainerLight,
|
||||||
|
onWarningContainerLight,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedDark = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningDark,
|
||||||
|
onWarningDark,
|
||||||
|
warningContainerDark,
|
||||||
|
onWarningContainerDark,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedLightMediumContrast = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningLightMediumContrast,
|
||||||
|
onWarningLightMediumContrast,
|
||||||
|
warningContainerLightMediumContrast,
|
||||||
|
onWarningContainerLightMediumContrast,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedLightHighContrast = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningLightHighContrast,
|
||||||
|
onWarningLightHighContrast,
|
||||||
|
warningContainerLightHighContrast,
|
||||||
|
onWarningContainerLightHighContrast,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedDarkMediumContrast = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningDarkMediumContrast,
|
||||||
|
onWarningDarkMediumContrast,
|
||||||
|
warningContainerDarkMediumContrast,
|
||||||
|
onWarningContainerDarkMediumContrast,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedDarkHighContrast = ExtendedColorScheme(
|
||||||
|
warning = ColorFamily(
|
||||||
|
warningDarkHighContrast,
|
||||||
|
onWarningDarkHighContrast,
|
||||||
|
warningContainerDarkHighContrast,
|
||||||
|
onWarningContainerDarkHighContrast,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ColorFamily(
|
data class ColorFamily(
|
||||||
val color: Color,
|
val color: Color,
|
||||||
@@ -254,12 +313,17 @@ val unspecified_scheme = ColorFamily(
|
|||||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun extendedColorScheme(): ExtendedColorScheme {
|
||||||
|
return if (isSystemInDarkTheme()) extendedDark else extendedLight
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTheme(
|
fun AppTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
// Dynamic color is available on Android 12+
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
content: @Composable() () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui
|
package ru.n08i40k.polytechnic.next.ui.widgets
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
@@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -39,9 +38,14 @@ import androidx.compose.ui.unit.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExpandableCard(
|
fun ExpandableCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
expanded: Boolean,
|
colors: CardColors = CardDefaults.cardColors(),
|
||||||
|
border: BorderStroke = BorderStroke(
|
||||||
|
Dp.Hairline,
|
||||||
|
MaterialTheme.colorScheme.inverseSurface
|
||||||
|
),
|
||||||
|
expanded: Boolean = false,
|
||||||
onExpandedChange: () -> Unit,
|
onExpandedChange: () -> Unit,
|
||||||
title: String,
|
title: @Composable () -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val transitionState = remember {
|
val transitionState = remember {
|
||||||
@@ -57,8 +61,8 @@ fun ExpandableCard(
|
|||||||
onExpandedChange()
|
onExpandedChange()
|
||||||
transitionState.targetState = expanded
|
transitionState.targetState = expanded
|
||||||
},
|
},
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
colors = colors,
|
||||||
border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)
|
border = border
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
ExpandableCardHeader(title, transition)
|
ExpandableCardHeader(title, transition)
|
||||||
@@ -67,6 +71,25 @@ fun ExpandableCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
colors: CardColors = CardDefaults.cardColors(),
|
||||||
|
border: BorderStroke = BorderStroke(
|
||||||
|
Dp.Hairline,
|
||||||
|
MaterialTheme.colorScheme.inverseSurface
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
colors = colors,
|
||||||
|
border = border
|
||||||
|
) {
|
||||||
|
ExpandableCardHeader(title, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpandableCardContent(
|
private fun ExpandableCardContent(
|
||||||
visible: Boolean = true,
|
visible: Boolean = true,
|
||||||
@@ -99,7 +122,7 @@ private fun ExpandableCardContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpandableCardTitle(text: String) {
|
fun ExpandableCardTitle(text: String) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -130,8 +153,8 @@ private fun ExpandableCardArrow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpandableCardHeader(
|
private fun ExpandableCardHeader(
|
||||||
title: String = "TODO",
|
title: @Composable () -> Unit,
|
||||||
transition: Transition<Boolean>
|
transition: Transition<Boolean>?
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -139,7 +162,8 @@ private fun ExpandableCardHeader(
|
|||||||
.padding(10.dp, 0.dp),
|
.padding(10.dp, 0.dp),
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
) {
|
) {
|
||||||
|
if (transition != null)
|
||||||
ExpandableCardArrow(transition)
|
ExpandableCardArrow(transition)
|
||||||
ExpandableCardTitle(title)
|
title()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getGroups(context: Context, onUpdated: (String?) -> Unit): ArrayList<String?> {
|
||||||
|
val groupPlaceholder = stringResource(R.string.loading)
|
||||||
|
|
||||||
|
val groups = remember { arrayListOf(null, groupPlaceholder) }
|
||||||
|
|
||||||
|
LaunchedEffect(groups) {
|
||||||
|
ScheduleGetGroupNames(context, {
|
||||||
|
groups.clear()
|
||||||
|
groups.addAll(it.names)
|
||||||
|
onUpdated(groups.getOrElse(0) { "TODO" }!!)
|
||||||
|
}, {
|
||||||
|
groups.clear()
|
||||||
|
groups.add(null)
|
||||||
|
groups.add(context.getString(R.string.failed_to_fetch_group_names))
|
||||||
|
onUpdated(groups[1]!!)
|
||||||
|
}).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun GroupSelector(
|
||||||
|
value: String? = "ИС-214/24",
|
||||||
|
isError: Boolean = false,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
onValueChange: (String?) -> Unit = {},
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = {
|
||||||
|
expanded = !readOnly && !expanded
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val groups = getGroups(LocalContext.current, onValueChange)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
label = { Text(stringResource(R.string.group)) },
|
||||||
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
||||||
|
value = value ?: groups.getOrElse(1) { "TODO" }!!,
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Email,
|
||||||
|
contentDescription = "group"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onValueChange = {},
|
||||||
|
isError = isError,
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
groups.forEach {
|
||||||
|
if (it == null)
|
||||||
|
return@forEach
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(it) },
|
||||||
|
onClick = {
|
||||||
|
if (groups.size > 0 && groups[0] != null)
|
||||||
|
onValueChange(it)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui
|
package ru.n08i40k.polytechnic.next.ui.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Error
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Info
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Warning
|
||||||
|
import ru.n08i40k.polytechnic.next.ui.theme.extendedColorScheme
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun NotificationCard(
|
||||||
|
level: Level = Level.SEVERE,
|
||||||
|
title: String = "Test",
|
||||||
|
content: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val titleComposable = @Composable {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val icon = when (level) {
|
||||||
|
Level.SEVERE -> AppIcons.Filled.Error
|
||||||
|
Level.WARNING -> AppIcons.Filled.Warning
|
||||||
|
else -> AppIcons.Filled.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(imageVector = icon, contentDescription = "Level")
|
||||||
|
Icon(imageVector = icon, contentDescription = "Level")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val colors = when (level) {
|
||||||
|
Level.WARNING -> {
|
||||||
|
val colorFamily = extendedColorScheme().warning
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = colorFamily.colorContainer,
|
||||||
|
contentColor = colorFamily.onColorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Level.SEVERE -> CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> CardDefaults.cardColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ExpandableCard(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded },
|
||||||
|
content = content,
|
||||||
|
title = titleComposable,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ExpandableCard(
|
||||||
|
title = titleComposable,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
|
import ru.n08i40k.polytechnic.next.model.UserRole.Companion.AcceptableUserRoles
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun RoleSelector(
|
||||||
|
value: UserRole = UserRole.STUDENT,
|
||||||
|
isError: Boolean = false,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
onValueChange: (UserRole) -> Unit = {},
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !readOnly && !expanded }
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
label = { Text(stringResource(R.string.role)) },
|
||||||
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
||||||
|
value = stringResource(value.stringId),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = value.icon,
|
||||||
|
contentDescription = "role icon"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onValueChange = {},
|
||||||
|
isError = isError,
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }) {
|
||||||
|
AcceptableUserRoles.forEach {
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = { Icon(it.icon, contentDescription = "Role icon") },
|
||||||
|
text = { Text(stringResource(it.stringId)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onValueChange(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package ru.n08i40k.polytechnic.next.work
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.Worker
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
|
||||||
|
|
||||||
class ScheduleClvAlarm(context: Context, workerParams: WorkerParameters) :
|
|
||||||
Worker(context, workerParams) {
|
|
||||||
override fun doWork(): Result {
|
|
||||||
val application = applicationContext as PolytechnicApplication
|
|
||||||
|
|
||||||
val result = runBlocking {
|
|
||||||
application
|
|
||||||
.container
|
|
||||||
.scheduleRepository
|
|
||||||
.getGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result is MyResult.Failure)
|
|
||||||
return Result.failure()
|
|
||||||
|
|
||||||
application.scheduleClvService((result as MyResult.Success).data)
|
|
||||||
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package ru.n08i40k.polytechnic.next.work
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.content.ContextCompat.startForegroundService
|
|
||||||
import androidx.work.Worker
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
|
||||||
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
|
|
||||||
|
|
||||||
class StartClvService(context: Context, workerParams: WorkerParameters) :
|
|
||||||
Worker(context, workerParams) {
|
|
||||||
override fun doWork(): Result {
|
|
||||||
val schedule = runBlocking {
|
|
||||||
(applicationContext as PolytechnicApplication)
|
|
||||||
.container
|
|
||||||
.scheduleRepository
|
|
||||||
.getGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule is MyResult.Failure)
|
|
||||||
return Result.success()
|
|
||||||
|
|
||||||
val intent = Intent(applicationContext, CurrentLessonViewService::class.java)
|
|
||||||
.apply {
|
|
||||||
putExtra("group", (schedule as MyResult.Success).data)
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationContext.stopService(
|
|
||||||
Intent(
|
|
||||||
applicationContext,
|
|
||||||
CurrentLessonViewService::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
startForegroundService(applicationContext, intent)
|
|
||||||
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground>
|
|
||||||
<inset
|
|
||||||
android:drawable="@drawable/logo"
|
|
||||||
android:inset="20dp" />
|
|
||||||
</foreground>
|
|
||||||
|
|
||||||
<background android:drawable="@color/white" />
|
|
||||||
|
|
||||||
<monochrome>
|
|
||||||
<inset
|
|
||||||
android:drawable="@drawable/logo"
|
|
||||||
android:inset="20dp" />
|
|
||||||
</monochrome>
|
|
||||||
</adaptive-icon>
|
|
||||||
19
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
19
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="81.5"
|
||||||
|
android:viewportHeight="81.5">
|
||||||
|
<group android:scaleX="0.5265958"
|
||||||
|
android:scaleY="0.5265958"
|
||||||
|
android:translateX="19.291224"
|
||||||
|
android:translateY="19.291224">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M51.89,64.09c0,1.22 -0.06,2.8 2.48,4.03 2.55,1.23 5.58,-2.19 7.04,-3.69 1.46,-1.5 5.46,-6.51 5.46,-7.91 0,-0.77 -1.56,-2.28 -2.77,-0.36l-2,3.14c-1.45,2.13 -8.07,9.83 -7.43,4.3 0.74,-6.42 6.07,-28.1 6.07,-28.76 0,-1.45 -2.03,-2.42 -2.88,-0.1l-4.98,24.31c-0.31,1.21 -0.49,2.44 -0.77,3.62 -0.14,0.61 -0.22,0.69 -0.22,1.43h0Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M5.33,39.39c0,1.71 2.35,1.81 3.01,-0.31 0.17,-0.53 0.17,-0.77 0.36,-1.28l1.6,-3.35c0.43,-0.76 0.78,-1.48 1.27,-2.22l4.21,-5.89c0.59,-0.79 1.09,-1.08 1.67,-1.82l2.21,-2.2c2.95,-2.54 5.98,-4.96 9.83,-6.18 11.36,-3.5 19.74,2.13 19.28,14.3 0,0.7 -1.94,1.18 -2.58,1.47 -4.35,1.9 -8.65,3.99 -13.11,5.69 -2.01,0.76 -3.26,1.1 -3.63,2.01 -0.37,0.91 0.13,1.85 1.01,1.98 0.88,0.12 2.75,-0.86 3.87,-1.33 1.08,-0.45 12.96,-6.07 14.07,-6.16 -0.24,1.97 -0.92,4.08 -1.58,5.93l-0.54,1.29c-2.27,5.24 -5.46,9.65 -9.08,14.04 -2.46,2.83 -5.74,6.02 -8.98,7.93l-5.24,2.5c-4.75,1.77 -10.16,0.07 -13.25,-3.7 -0.41,-0.5 -0.69,-1.31 -0.84,-1.91 -0.22,-0.93 0.07,-2.48 -0.6,-3.24 -0.85,-0.96 -2.41,-0.43 -2.41,0.92 0,5.07 2.79,7.94 7.15,10.11 3.8,1.89 8.59,1.37 12.37,-0.14l1.78,-0.79c5.11,-2.78 9.5,-6.62 13.14,-11.13 3.48,-4.27 6.64,-8.87 8.74,-14.02l1.11,-3.11c0.29,-0.82 0.43,-1.38 0.57,-2.17 0.98,-3.89 -0.32,-3.35 2.5,-4.66 7.13,-3.13 14.12,-6.43 21.3,-9.47 0.77,-0.33 1.43,-0.41 1.6,-1.34 0.1,-0.55 -0.21,-1.32 -0.97,-1.54 -0.75,-0.22 -1.36,0.26 -2.07,0.55 -7.55,3.07 -15.9,7.61 -19.81,8.78 -2.85,1.53 -1.9,-0.01 -2.42,-4.66 -1.17,-6.1 -5.82,-10.61 -12,-11.61 -6.59,-1.2 -12.17,1.22 -17.45,4.66l-0.4,0.34s-0.07,0.06 -0.1,0.08l-2.52,2.08c-3.35,2.69 -6,6.24 -8.39,9.8 -1.41,2.13 -4.7,7.55 -4.7,9.79Z"
|
||||||
|
tools:ignore="VectorPath" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground>
|
|
||||||
<inset
|
|
||||||
android:drawable="@drawable/logo"
|
|
||||||
android:inset="20dp" />
|
|
||||||
</foreground>
|
|
||||||
|
|
||||||
<background android:drawable="@color/white" />
|
|
||||||
|
|
||||||
<monochrome>
|
|
||||||
<inset
|
|
||||||
android:drawable="@drawable/logo"
|
|
||||||
android:inset="20dp" />
|
|
||||||
</monochrome>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="81.5dp"
|
|
||||||
android:height="81.5dp"
|
|
||||||
android:viewportWidth="81.5"
|
|
||||||
android:viewportHeight="81.5">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M51.89,64.09c0,1.22 -0.06,2.8 2.48,4.03 2.55,1.23 5.58,-2.19 7.04,-3.69 1.46,-1.5 5.46,-6.51 5.46,-7.91 0,-0.77 -1.56,-2.28 -2.77,-0.36l-2,3.14c-1.45,2.13 -8.07,9.83 -7.43,4.3 0.74,-6.42 6.07,-28.1 6.07,-28.76 0,-1.45 -2.03,-2.42 -2.88,-0.1l-4.98,24.31c-0.31,1.21 -0.49,2.44 -0.77,3.62 -0.14,0.61 -0.22,0.69 -0.22,1.43h0Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M5.33,39.39c0,1.71 2.35,1.81 3.01,-0.31 0.17,-0.53 0.17,-0.77 0.36,-1.28l1.6,-3.35c0.43,-0.76 0.78,-1.48 1.27,-2.22l4.21,-5.89c0.59,-0.79 1.09,-1.08 1.67,-1.82l2.21,-2.2c2.95,-2.54 5.98,-4.96 9.83,-6.18 11.36,-3.5 19.74,2.13 19.28,14.3 0,0.7 -1.94,1.18 -2.58,1.47 -4.35,1.9 -8.65,3.99 -13.11,5.69 -2.01,0.76 -3.26,1.1 -3.63,2.01 -0.37,0.91 0.13,1.85 1.01,1.98 0.88,0.12 2.75,-0.86 3.87,-1.33 1.08,-0.45 12.96,-6.07 14.07,-6.16 -0.24,1.97 -0.92,4.08 -1.58,5.93l-0.54,1.29c-2.27,5.24 -5.46,9.65 -9.08,14.04 -2.46,2.83 -5.74,6.02 -8.98,7.93l-5.24,2.5c-4.75,1.77 -10.16,0.07 -13.25,-3.7 -0.41,-0.5 -0.69,-1.31 -0.84,-1.91 -0.22,-0.93 0.07,-2.48 -0.6,-3.24 -0.85,-0.96 -2.41,-0.43 -2.41,0.92 0,5.07 2.79,7.94 7.15,10.11 3.8,1.89 8.59,1.37 12.37,-0.14l1.78,-0.79c5.11,-2.78 9.5,-6.62 13.14,-11.13 3.48,-4.27 6.64,-8.87 8.74,-14.02l1.11,-3.11c0.29,-0.82 0.43,-1.38 0.57,-2.17 0.98,-3.89 -0.32,-3.35 2.5,-4.66 7.13,-3.13 14.12,-6.43 21.3,-9.47 0.77,-0.33 1.43,-0.41 1.6,-1.34 0.1,-0.55 -0.21,-1.32 -0.97,-1.54 -0.75,-0.22 -1.36,0.26 -2.07,0.55 -7.55,3.07 -15.9,7.61 -19.81,8.78 -2.85,1.53 -1.9,-0.01 -2.42,-4.66 -1.17,-6.1 -5.82,-10.61 -12,-11.61 -6.59,-1.2 -12.17,1.22 -17.45,4.66l-0.4,0.34s-0.07,0.06 -0.1,0.08l-2.52,2.08c-3.35,2.69 -6,6.24 -8.39,9.8 -1.41,2.13 -4.7,7.55 -4.7,9.79Z"/>
|
|
||||||
</vector>
|
|
||||||
69
app/src/main/res/drawable/schedule.xml
Normal file
69
app/src/main/res/drawable/schedule.xml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="18.80dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="85.47">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.99,12.42L85.01,12.42A13.04,13.04 0,0 1,98.05 25.46L98.05,70.48A13.04,13.04 0,0 1,85.01 83.52L14.99,83.52A13.04,13.04 0,0 1,1.95 70.48L1.95,25.46A13.04,13.04 0,0 1,14.99 12.42z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="3.9016"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M100,30.35L0,30.35"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m17,10.81v-4.11c0,-2.61 1.79,-4.7 4.01,-4.7h7.3c2.22,0 4.01,2.1 4.01,4.7v4.11"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m67.68,10.81v-4.11c0,-2.61 1.79,-4.7 4.01,-4.7h7.3c2.22,0 4.01,2.1 4.01,4.7v4.11"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.51,45.64m-4.26,0a4.26,4.26 89.5,1 1,8.52 0a4.26,4.26 89.5,1 1,-8.52 0"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34.4,41.38L80.52,41.38A4.23,4.23 90.46,0 1,84.75 45.61L84.75,45.67A4.23,4.23 90.46,0 1,80.52 49.9L34.4,49.9A4.23,4.23 90.46,0 1,30.17 45.67L30.17,45.61A4.23,4.23 90.46,0 1,34.4 41.38z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4.83"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.51,56.98m-4.26,0a4.26,4.26 89.5,1 1,8.52 0a4.26,4.26 89.5,1 1,-8.52 0"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34.4,52.72L80.52,52.72A4.23,4.23 90.46,0 1,84.75 56.95L84.75,57.01A4.23,4.23 90.46,0 1,80.52 61.24L34.4,61.24A4.23,4.23 90.46,0 1,30.17 57.01L30.17,56.95A4.23,4.23 90.46,0 1,34.4 52.72z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4.83"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.51,68.32m-4.26,0a4.26,4.26 89.5,1 1,8.52 0a4.26,4.26 89.5,1 1,-8.52 0"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34.4,64.06L80.52,64.06A4.23,4.23 90.46,0 1,84.75 68.29L84.75,68.35A4.23,4.23 90.46,0 1,80.52 72.58L34.4,72.58A4.23,4.23 90.46,0 1,30.17 68.35L30.17,68.29A4.23,4.23 90.46,0 1,34.4 64.06z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="4.83"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -3,10 +3,9 @@
|
|||||||
<string name="app_name">Политехникум</string>
|
<string name="app_name">Политехникум</string>
|
||||||
<string name="username">Имя пользователя</string>
|
<string name="username">Имя пользователя</string>
|
||||||
<string name="password">Пароль</string>
|
<string name="password">Пароль</string>
|
||||||
<string name="register">Зарегистрироваться</string>
|
<string name="proceed">Продолжить</string>
|
||||||
<string name="login">Авторизоваться</string>
|
<string name="sign_in_title">Авторизация</string>
|
||||||
<string name="login_title">Авторизация</string>
|
<string name="sign_up_title">Регистрация</string>
|
||||||
<string name="register_title">Регистрация</string>
|
|
||||||
<string name="not_registered">Не зарегистрированы?</string>
|
<string name="not_registered">Не зарегистрированы?</string>
|
||||||
<string name="already_registered">Уже зарегистрированы?</string>
|
<string name="already_registered">Уже зарегистрированы?</string>
|
||||||
<string name="reload">Перезагрузить</string>
|
<string name="reload">Перезагрузить</string>
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
<string name="role_admin">Администратор</string>
|
<string name="role_admin">Администратор</string>
|
||||||
<string name="group">Группа</string>
|
<string name="group">Группа</string>
|
||||||
<string name="role">Роль</string>
|
<string name="role">Роль</string>
|
||||||
<string name="day_null">Расписание ещё не обновилось.</string>
|
<string name="day_null">На этот день расписания ещё нет!</string>
|
||||||
<string name="old_password">Старый пароль</string>
|
<string name="old_password">Старый пароль</string>
|
||||||
<string name="new_password">Новый пароль</string>
|
<string name="new_password">Новый пароль</string>
|
||||||
<string name="loading">Загрузка…</string>
|
<string name="loading">Загрузка…</string>
|
||||||
@@ -65,4 +64,13 @@
|
|||||||
<string name="in_gym_lc">в спорт-зале</string>
|
<string name="in_gym_lc">в спорт-зале</string>
|
||||||
<string name="lessons_not_started">Пары ещё не начались</string>
|
<string name="lessons_not_started">Пары ещё не начались</string>
|
||||||
<string name="waiting_for_day_start_notification_title">До начала пар %1$d ч. %2$d мин.</string>
|
<string name="waiting_for_day_start_notification_title">До начала пар %1$d ч. %2$d мин.</string>
|
||||||
|
<string name="outdated_schedule">Вы просматриваете устаревшее расписание!</string>
|
||||||
|
<string name="invalid_credentials">Некорректное имя пользователя или пароль!</string>
|
||||||
|
<string name="timed_out">Неудалось отправить запрос, попробуйте позже!</string>
|
||||||
|
<string name="app_too_old">Пожалуйста обновите приложение!</string>
|
||||||
|
<string name="unknown_error">Произошла неизвестная ошибка! Попробуйте позже.</string>
|
||||||
|
<string name="failed_to_fetch_group_names">Не удалось получить список названий групп!</string>
|
||||||
|
<string name="already_exists">Пользователь с таким именем уже зарегистрирован!</string>
|
||||||
|
<string name="group_does_not_exists">Группа с таким названием не существует!</string>
|
||||||
|
<string name="no_connection">Нет подключения к интернету!</string>
|
||||||
</resources>
|
</resources>
|
||||||
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
|
|
||||||
<string name="username">Username</string>
|
<string name="username">Username</string>
|
||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
<string name="register">Register</string>
|
<string name="proceed">Proceed</string>
|
||||||
<string name="login">Login</string>
|
<string name="sign_in_title">Sign In</string>
|
||||||
<string name="login_title">Login</string>
|
<string name="sign_up_title">Sign Up</string>
|
||||||
<string name="register_title">Registration</string>
|
|
||||||
<string name="not_registered">Not registered?</string>
|
<string name="not_registered">Not registered?</string>
|
||||||
<string name="already_registered">Already registered?</string>
|
<string name="already_registered">Already registered?</string>
|
||||||
<string name="reload">Reload</string>
|
<string name="reload">Reload</string>
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
<string name="role_admin">Administrator</string>
|
<string name="role_admin">Administrator</string>
|
||||||
<string name="group">Group</string>
|
<string name="group">Group</string>
|
||||||
<string name="role">Role</string>
|
<string name="role">Role</string>
|
||||||
<string name="day_null">Schedule not updated yet.</string>
|
<string name="day_null">There is no schedule for this day yet!</string>
|
||||||
<string name="old_password">Old password</string>
|
<string name="old_password">Old password</string>
|
||||||
<string name="new_password">New password</string>
|
<string name="new_password">New password</string>
|
||||||
<string name="change_password">Change password</string>
|
<string name="change_password">Change password</string>
|
||||||
@@ -65,4 +64,13 @@
|
|||||||
<string name="in_gym_lc">in gym</string>
|
<string name="in_gym_lc">in gym</string>
|
||||||
<string name="lessons_not_started">Lessons haven\'t started yet</string>
|
<string name="lessons_not_started">Lessons haven\'t started yet</string>
|
||||||
<string name="waiting_for_day_start_notification_title">%1$d h. %2$d min. before lessons start</string>
|
<string name="waiting_for_day_start_notification_title">%1$d h. %2$d min. before lessons start</string>
|
||||||
|
<string name="outdated_schedule">You are viewing an outdated schedule!</string>
|
||||||
|
<string name="invalid_credentials">Invalid credentials!</string>
|
||||||
|
<string name="timed_out">Failed to send request, try again later!</string>
|
||||||
|
<string name="app_too_old">Please update the application!</string>
|
||||||
|
<string name="unknown_error">An unknown error has occurred! Please try again later.</string>
|
||||||
|
<string name="failed_to_fetch_group_names">Failed to get list of group names!</string>
|
||||||
|
<string name="already_exists">A user with this name is already registered!</string>
|
||||||
|
<string name="group_does_not_exists">A group with this name does not exist!</string>
|
||||||
|
<string name="no_connection">No internet connection!</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user