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:
@@ -33,8 +33,8 @@ android {
|
||||
applicationId = "ru.n08i40k.polytechnic.next"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 13
|
||||
versionName = "1.7.1"
|
||||
versionCode = 15
|
||||
versionName = "1.8.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -12,31 +12,17 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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
|
||||
android:name=".PolytechnicApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.PolytechnicNext"
|
||||
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
|
||||
android:name=".service.MyFirebaseMessagingService"
|
||||
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
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
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 java.util.Calendar
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
@@ -34,84 +27,4 @@ class PolytechnicApplication : Application() {
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== 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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
|
||||
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
|
||||
override suspend fun getGroup(): MyResult<Group> {
|
||||
return 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!"))
|
||||
|
||||
override suspend fun getGroup(): MyResult<Group> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val response = tryFuture {
|
||||
ScheduleGet(
|
||||
ScheduleGet.RequestDto(groupName),
|
||||
context,
|
||||
it,
|
||||
it
|
||||
@@ -37,5 +25,4 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
|
||||
is MyResult.Success -> MyResult.Success(response.data.group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ open class RequestBase(
|
||||
override fun getHeaders(): MutableMap<String, String> {
|
||||
val headers = mutableMapOf<String, String>()
|
||||
headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
headers["version"] = "1"
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
@@ -26,4 +26,11 @@ fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
|
||||
} catch (exception: TimeoutException) {
|
||||
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>,
|
||||
errorListener: Response.ErrorListener?,
|
||||
) : AuthorizedRequest(context, method, url, {
|
||||
runBlocking {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
(context as PolytechnicApplication)
|
||||
.container.networkCacheRepository.put(url, it)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class AuthLogin(
|
||||
class AuthSignIn(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
@@ -23,9 +23,16 @@ class AuthLogin(
|
||||
data class RequestDto(val username: String, val password: String)
|
||||
|
||||
@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 {
|
||||
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.network.RequestBase
|
||||
|
||||
class AuthRegister(
|
||||
class AuthSignUp(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
@@ -3,13 +3,11 @@ package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||
|
||||
class ScheduleGet(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
@@ -29,8 +27,4 @@ class ScheduleGet(
|
||||
val group: Group,
|
||||
val lastChangedDays: ArrayList<Int>,
|
||||
)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,11 @@ class ScheduleGetCacheStatus(
|
||||
val lastCacheUpdate: 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 kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class ScheduleGetGroupNames(
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : CachedRequest(
|
||||
) : RequestBase(
|
||||
context,
|
||||
Method.GET,
|
||||
"schedule/get-group-names",
|
||||
|
||||
@@ -25,4 +25,11 @@ class ScheduleUpdate(
|
||||
override fun getBody(): ByteArray {
|
||||
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.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.core.content.ContextCompat.startForegroundService
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
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.Group
|
||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
|
||||
import ru.n08i40k.polytechnic.next.utils.getDayMinutes
|
||||
import ru.n08i40k.polytechnic.next.work.StartClvService
|
||||
import java.util.Calendar
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||
class CurrentLessonViewService : Service() {
|
||||
@@ -30,19 +29,31 @@ class CurrentLessonViewService : Service() {
|
||||
private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
|
||||
private const val UPDATE_INTERVAL = 60_000L
|
||||
|
||||
fun startService(appContext: Context) {
|
||||
if (!(appContext as PolytechnicApplication).hasNotificationPermission())
|
||||
suspend fun startService(application: PolytechnicApplication) {
|
||||
if (!application.hasNotificationPermission())
|
||||
return
|
||||
|
||||
if (Calendar.getInstance()
|
||||
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
|
||||
.get(Calendar.MINUTE) < 420)
|
||||
val schedule =
|
||||
application
|
||||
.container
|
||||
.scheduleRepository
|
||||
.getGroup()
|
||||
|
||||
if (schedule is MyResult.Failure)
|
||||
return
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<StartClvService>()
|
||||
.build()
|
||||
val intent = Intent(application, CurrentLessonViewService::class.java)
|
||||
.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 updateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
val logger = Logger.getLogger("CLV.updateRunnable")
|
||||
|
||||
if (day == null || day!!.nonNullIndices.isEmpty()) {
|
||||
logger.warning("Stopping, because day is null or empty!")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
@@ -74,7 +88,7 @@ class CurrentLessonViewService : Service() {
|
||||
if (currentLesson == null && nextLesson == null) {
|
||||
val notification = NotificationCompat
|
||||
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setSmallIcon(R.drawable.schedule)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentTitle(getString(R.string.lessons_end_notification_title))
|
||||
.setContentText(getString(R.string.lessons_end_notification_description))
|
||||
@@ -146,7 +160,7 @@ class CurrentLessonViewService : Service() {
|
||||
): Notification {
|
||||
return NotificationCompat
|
||||
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setSmallIcon(R.drawable.schedule)
|
||||
.setContentTitle(title ?: getString(R.string.lesson_notification_title))
|
||||
.setContentText(description ?: getString(R.string.lesson_notification_description))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
@@ -159,26 +173,29 @@ class CurrentLessonViewService : Service() {
|
||||
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) {
|
||||
logger.warning("Stopping, because group is null")
|
||||
stopSelf()
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
val currentDay = group.current
|
||||
if (currentDay == null || currentDay.nonNullIndices.isEmpty()) {
|
||||
logger.warning("Stopping, because current day is null or empty")
|
||||
stopSelf()
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.day == null) {
|
||||
val nowMinutes = Calendar.getInstance().getDayMinutes()
|
||||
|
||||
if (currentDay.first!!.time.start - nowMinutes > 30
|
||||
|| currentDay.last!!.time.end < nowMinutes) {
|
||||
stopSelf()
|
||||
return false
|
||||
}
|
||||
val nowMinutes = Calendar.getInstance().getDayMinutes()
|
||||
if (nowMinutes < ((5 * 60) + 30)
|
||||
|| currentDay.last!!.time.end < nowMinutes
|
||||
) {
|
||||
logger.warning("Stopping, because service started outside of acceptable time range!")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
|
||||
this.day = currentDay
|
||||
@@ -186,7 +203,7 @@ class CurrentLessonViewService : Service() {
|
||||
this.handler.removeCallbacks(updateRunnable)
|
||||
updateRunnable.run()
|
||||
|
||||
return true
|
||||
logger.info("Running...")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -201,16 +218,14 @@ class CurrentLessonViewService : Service() {
|
||||
val notification = createNotification()
|
||||
startForeground(NOTIFICATION_STATUS_ID, notification)
|
||||
|
||||
if (!updateSchedule(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("group", Group::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("group")
|
||||
}
|
||||
)
|
||||
updateSchedule(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("group", Group::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("group")
|
||||
}
|
||||
)
|
||||
updateRunnable.run()
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -17,13 +17,19 @@ import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
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.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
|
||||
import ru.n08i40k.polytechnic.next.work.ScheduleClvAlarm
|
||||
import java.time.Duration
|
||||
|
||||
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
|
||||
@@ -79,7 +85,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
}
|
||||
|
||||
notify(id.hashCode(), notification)
|
||||
CurrentLessonViewService.startService(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +95,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
"schedule-update" -> {
|
||||
sendNotification(
|
||||
NotificationChannels.SCHEDULE_UPDATE,
|
||||
R.drawable.logo,
|
||||
R.drawable.schedule,
|
||||
getString(R.string.schedule_update_title),
|
||||
getString(
|
||||
if (message.data["replaced"] == "true")
|
||||
@@ -101,24 +106,19 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
message.data["etag"]
|
||||
)
|
||||
}
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<ScheduleClvAlarm>()
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(applicationContext)
|
||||
.enqueue(request)
|
||||
"lessons-start" -> {
|
||||
scope.launch {
|
||||
CurrentLessonViewService
|
||||
.startService(applicationContext as PolytechnicApplication)
|
||||
}
|
||||
}
|
||||
|
||||
"app-update" -> {
|
||||
sendNotification(
|
||||
NotificationChannels.APP_UPDATE,
|
||||
R.drawable.logo,
|
||||
R.drawable.download,
|
||||
getString(R.string.app_update_title, message.data["version"]),
|
||||
getString(R.string.app_update_description),
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
|
||||
@@ -36,7 +36,6 @@ import kotlinx.coroutines.launch
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
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.work.FcmUpdateCallbackWorker
|
||||
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
|
||||
@@ -101,21 +100,6 @@ class MainActivity : ComponentActivity() {
|
||||
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) {
|
||||
val tag = "schedule-update"
|
||||
@@ -182,7 +166,6 @@ class MainActivity : ComponentActivity() {
|
||||
setupFirebaseConfig()
|
||||
|
||||
handleUpdate()
|
||||
scheduleAlarm()
|
||||
|
||||
setContent {
|
||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||
|
||||
@@ -1,504 +1,113 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.auth
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.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.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
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.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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
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.map
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
|
||||
@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)
|
||||
@Composable
|
||||
fun AuthForm(
|
||||
mutableIsLogin: MutableState<Boolean> = mutableStateOf(true),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
appNavController: NavHostController = rememberNavController(),
|
||||
onPendingSnackbar: (String) -> Unit = {},
|
||||
) {
|
||||
var isLogin by mutableIsLogin
|
||||
val navController = rememberNavController()
|
||||
|
||||
val mutableVisible = remember { mutableStateOf(true) }
|
||||
var visible by mutableVisible
|
||||
val modifier = Modifier.fillMaxSize()
|
||||
|
||||
val animatedAlpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1.0f else 0f, label = "alpha"
|
||||
)
|
||||
|
||||
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 = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||
)
|
||||
|
||||
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!"
|
||||
NavHost(
|
||||
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
|
||||
)
|
||||
)
|
||||
},
|
||||
exitTransition = {
|
||||
fadeOut(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
it.printStackTrace()
|
||||
}).send()
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
||||
fun AuthScreen(appNavController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -506,14 +115,20 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
||||
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 scope = rememberCoroutineScope()
|
||||
|
||||
val onPendingSnackbar: (String) -> Unit = {
|
||||
scope.launch { snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Long) }
|
||||
}
|
||||
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
@@ -525,14 +140,10 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Card {
|
||||
AuthForm(
|
||||
mutableIsLogin,
|
||||
navController,
|
||||
scope,
|
||||
snackbarHostState
|
||||
)
|
||||
}
|
||||
AuthForm(
|
||||
appNavController,
|
||||
onPendingSnackbar
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,3 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.icons
|
||||
|
||||
object AppIcons
|
||||
object AppIcons
|
||||
|
||||
@@ -6,4 +6,4 @@ object FilledGroup
|
||||
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
val AppIcons.Filled: FilledGroup
|
||||
get() = FilledGroup
|
||||
get() = FilledGroup
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
|
||||
|
||||
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
val FilledGroup.Download: ImageVector
|
||||
get() {
|
||||
if (_download != null) {
|
||||
return _download!!
|
||||
}
|
||||
_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
|
||||
).apply {
|
||||
path(
|
||||
@@ -55,5 +56,4 @@ val FilledGroup.Download: ImageVector
|
||||
return _download!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
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
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
|
||||
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
val FilledGroup.Telegram: ImageVector
|
||||
get() {
|
||||
if (_telegram != null) {
|
||||
return _telegram!!
|
||||
}
|
||||
_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
|
||||
).apply {
|
||||
path(
|
||||
@@ -49,5 +50,4 @@ val FilledGroup.Telegram: ImageVector
|
||||
return _telegram!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
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,21 +116,21 @@ private fun NavHostContainer(
|
||||
)
|
||||
)
|
||||
},
|
||||
builder = {
|
||||
composable("profile") {
|
||||
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
|
||||
}
|
||||
) {
|
||||
composable("profile") {
|
||||
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
|
||||
}
|
||||
|
||||
composable("schedule") {
|
||||
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
|
||||
}
|
||||
composable("schedule") {
|
||||
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
|
||||
}
|
||||
|
||||
if (scheduleReplacerViewModel != null) {
|
||||
composable("replacer") {
|
||||
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
|
||||
}
|
||||
if (scheduleReplacerViewModel != null) {
|
||||
composable("replacer") {
|
||||
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLink(context: Context, link: String) {
|
||||
@@ -260,7 +260,11 @@ fun MainScreen(
|
||||
viewModel(
|
||||
factory = ProfileViewModel.provideFactory(
|
||||
profileRepository = mainViewModel.appContainer.profileRepository,
|
||||
onUnauthorized = { appNavController.navigate("auth") })
|
||||
onUnauthorized = {
|
||||
appNavController.navigate("auth") {
|
||||
popUpTo("main") { inclusive = true }
|
||||
}
|
||||
})
|
||||
)
|
||||
LocalContext.current.profileViewModel = profileViewModel
|
||||
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.profile
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.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.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
|
||||
@@ -36,7 +24,7 @@ import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
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 {
|
||||
NOT_EXISTS
|
||||
@@ -57,80 +45,6 @@ private fun tryChangeGroup(
|
||||
}).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)
|
||||
@Composable
|
||||
internal fun ChangeGroupDialog(
|
||||
@@ -141,7 +55,7 @@ internal fun ChangeGroupDialog(
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
var group by remember { mutableStateOf("ИС-214/23") }
|
||||
var group by remember { mutableStateOf<String?>(profile.group) }
|
||||
var groupError by remember { mutableStateOf(false) }
|
||||
|
||||
var processing by remember { mutableStateOf(false) }
|
||||
@@ -165,7 +79,7 @@ internal fun ChangeGroupDialog(
|
||||
|
||||
tryChangeGroup(
|
||||
context = context,
|
||||
group = group,
|
||||
group = group!!,
|
||||
onError = {
|
||||
when (it) {
|
||||
ChangeGroupError.NOT_EXISTS -> {
|
||||
@@ -178,7 +92,7 @@ internal fun ChangeGroupDialog(
|
||||
onSuccess = onChange
|
||||
)
|
||||
},
|
||||
enabled = !(groupError || processing)
|
||||
enabled = !(groupError || processing) && group != null
|
||||
) {
|
||||
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.hilt.navigation.compose.hiltViewModel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
@@ -187,6 +188,10 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
|
||||
context.settingsDataStore.updateData {
|
||||
it.toBuilder().setGroup(group).build()
|
||||
}
|
||||
(context.applicationContext as PolytechnicApplication)
|
||||
.container
|
||||
.networkCacheRepository
|
||||
.clear()
|
||||
}
|
||||
context.profileViewModel!!.refreshProfile {
|
||||
scheduleViewModel.refreshGroup()
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
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.ProfileViewModel
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
|
||||
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.ScheduleReplacerViewModel
|
||||
|
||||
|
||||
@@ -33,23 +33,15 @@ import kotlinx.coroutines.flow.flow
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Day
|
||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||
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
|
||||
private fun getMinutes(): Flow<Int> {
|
||||
private fun getCurrentLessonIdx(day: Day?): Flow<Int> {
|
||||
val value by remember {
|
||||
derivedStateOf {
|
||||
flow {
|
||||
while (true) {
|
||||
emit(getCurrentMinutes())
|
||||
emit(day?.currentIdx ?: -1)
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
@@ -59,22 +51,6 @@ private fun getMinutes(): Flow<Int> {
|
||||
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)
|
||||
@Composable
|
||||
fun DayCard(
|
||||
@@ -99,8 +75,8 @@ fun DayCard(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor =
|
||||
if (current) MaterialTheme.colorScheme.inverseSurface
|
||||
else MaterialTheme.colorScheme.surface
|
||||
if (current) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
|
||||
) {
|
||||
@@ -109,14 +85,10 @@ fun DayCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(R.string.day_null),
|
||||
color =
|
||||
if (current) MaterialTheme.colorScheme.inverseOnSurface
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
text = stringResource(R.string.day_null)
|
||||
)
|
||||
return@Card
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -124,7 +96,8 @@ fun DayCard(
|
||||
text = day.name,
|
||||
)
|
||||
|
||||
val currentLessonIdx = calculateCurrentLessonIdx(day.lessons)
|
||||
val currentLessonIdx by getCurrentLessonIdx(if (current) day else null)
|
||||
.collectAsStateWithLifecycle(0)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -1,21 +1,50 @@
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.Locale
|
||||
import java.util.logging.Level
|
||||
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
|
||||
@Composable
|
||||
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
||||
@@ -23,31 +52,38 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
||||
val calendarDay = currentDay
|
||||
.coerceAtLeast(0)
|
||||
.coerceAtMost(group.days.size - 1)
|
||||
|
||||
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size })
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(horizontal = 20.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier.height(600.dp)
|
||||
) { page ->
|
||||
DayCard(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
val offset = pagerState.getOffsetDistanceInPages(page).absoluteValue
|
||||
Column {
|
||||
if (!isCurrentWeek(group)) {
|
||||
NotificationCard(
|
||||
level = Level.WARNING,
|
||||
title = stringResource(R.string.outdated_schedule)
|
||||
)
|
||||
}
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(horizontal = 20.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier.height(600.dp).padding(top = 5.dp)
|
||||
) { page ->
|
||||
DayCard(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
val offset = pagerState.getOffsetDistanceInPages(page).absoluteValue
|
||||
|
||||
lerp(
|
||||
start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
).also { scale ->
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
alpha = lerp(
|
||||
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
)
|
||||
},
|
||||
day = group.days[page],
|
||||
current = currentDay == page
|
||||
)
|
||||
lerp(
|
||||
start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
).also { scale ->
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
alpha = lerp(
|
||||
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
)
|
||||
},
|
||||
day = group.days[page],
|
||||
current = currentDay == page
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
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.ScheduleViewModel
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
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.util.Date
|
||||
import java.util.Locale
|
||||
@@ -49,7 +50,7 @@ fun UpdateInfo(
|
||||
ExpandableCard(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
title = stringResource(R.string.update_info_header)
|
||||
title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val primaryLight = Color(0xFF4C662B)
|
||||
@@ -218,9 +217,33 @@ val surfaceContainerDarkHighContrast = Color(0xFF1E201A)
|
||||
val surfaceContainerHighDarkHighContrast = Color(0xFF282B24)
|
||||
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.platform.LocalContext
|
||||
|
||||
@Immutable
|
||||
data class ExtendedColorScheme(
|
||||
val warning: ColorFamily,
|
||||
)
|
||||
|
||||
private val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
@@ -242,6 +247,60 @@ private val highContrastDarkColorScheme = darkColorScheme(
|
||||
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
|
||||
data class ColorFamily(
|
||||
val color: Color,
|
||||
@@ -254,12 +313,17 @@ val unspecified_scheme = ColorFamily(
|
||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun extendedColorScheme(): ExtendedColorScheme {
|
||||
return if (isSystemInDarkTheme()) extendedDark else extendedLight
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
content: @Composable() () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
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.core.MutableTransitionState
|
||||
@@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -39,9 +38,14 @@ import androidx.compose.ui.unit.dp
|
||||
@Composable
|
||||
fun ExpandableCard(
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean,
|
||||
colors: CardColors = CardDefaults.cardColors(),
|
||||
border: BorderStroke = BorderStroke(
|
||||
Dp.Hairline,
|
||||
MaterialTheme.colorScheme.inverseSurface
|
||||
),
|
||||
expanded: Boolean = false,
|
||||
onExpandedChange: () -> Unit,
|
||||
title: String,
|
||||
title: @Composable () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val transitionState = remember {
|
||||
@@ -57,8 +61,8 @@ fun ExpandableCard(
|
||||
onExpandedChange()
|
||||
transitionState.targetState = expanded
|
||||
},
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)
|
||||
colors = colors,
|
||||
border = border
|
||||
) {
|
||||
Column {
|
||||
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
|
||||
private fun ExpandableCardContent(
|
||||
visible: Boolean = true,
|
||||
@@ -99,7 +122,7 @@ private fun ExpandableCardContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandableCardTitle(text: String) {
|
||||
fun ExpandableCardTitle(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
@@ -130,8 +153,8 @@ private fun ExpandableCardArrow(
|
||||
|
||||
@Composable
|
||||
private fun ExpandableCardHeader(
|
||||
title: String = "TODO",
|
||||
transition: Transition<Boolean>
|
||||
title: @Composable () -> Unit,
|
||||
transition: Transition<Boolean>?
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -139,7 +162,8 @@ private fun ExpandableCardHeader(
|
||||
.padding(10.dp, 0.dp),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
ExpandableCardArrow(transition)
|
||||
ExpandableCardTitle(title)
|
||||
if (transition != null)
|
||||
ExpandableCardArrow(transition)
|
||||
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.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="username">Имя пользователя</string>
|
||||
<string name="password">Пароль</string>
|
||||
<string name="register">Зарегистрироваться</string>
|
||||
<string name="login">Авторизоваться</string>
|
||||
<string name="login_title">Авторизация</string>
|
||||
<string name="register_title">Регистрация</string>
|
||||
<string name="proceed">Продолжить</string>
|
||||
<string name="sign_in_title">Авторизация</string>
|
||||
<string name="sign_up_title">Регистрация</string>
|
||||
<string name="not_registered">Не зарегистрированы?</string>
|
||||
<string name="already_registered">Уже зарегистрированы?</string>
|
||||
<string name="reload">Перезагрузить</string>
|
||||
@@ -23,7 +22,7 @@
|
||||
<string name="role_admin">Администратор</string>
|
||||
<string name="group">Группа</string>
|
||||
<string name="role">Роль</string>
|
||||
<string name="day_null">Расписание ещё не обновилось.</string>
|
||||
<string name="day_null">На этот день расписания ещё нет!</string>
|
||||
<string name="old_password">Старый пароль</string>
|
||||
<string name="new_password">Новый пароль</string>
|
||||
<string name="loading">Загрузка…</string>
|
||||
@@ -65,4 +64,13 @@
|
||||
<string name="in_gym_lc">в спорт-зале</string>
|
||||
<string name="lessons_not_started">Пары ещё не начались</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>
|
||||
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="password">Password</string>
|
||||
<string name="register">Register</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_title">Login</string>
|
||||
<string name="register_title">Registration</string>
|
||||
<string name="proceed">Proceed</string>
|
||||
<string name="sign_in_title">Sign In</string>
|
||||
<string name="sign_up_title">Sign Up</string>
|
||||
<string name="not_registered">Not registered?</string>
|
||||
<string name="already_registered">Already registered?</string>
|
||||
<string name="reload">Reload</string>
|
||||
@@ -23,7 +22,7 @@
|
||||
<string name="role_admin">Administrator</string>
|
||||
<string name="group">Group</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="new_password">New password</string>
|
||||
<string name="change_password">Change password</string>
|
||||
@@ -65,4 +64,13 @@
|
||||
<string name="in_gym_lc">in gym</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="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>
|
||||
Reference in New Issue
Block a user