Удалёно большинство классов относящихся к CustomLessonViewService:
- AlarmReceiver
- BootCompletedBroadcastReceiver
- ScheduleClvAlarm
- StartClvService

CustomLessonViewService теперь запускается сервером в определённое время.

Переработаны формы авторизации и регистрации.

В форме регистрации теперь можно выбрать свою группу из выпадающего списка, а не вводить вручную.

Исправлен недочёт, из-за которого можно было вернуться к форме авторизации нажимая кнопку назад (или делать свайп для того же эффекта).

Немного изменён логотип приложения.

Изменена иконка уведомлений на самодельную.
This commit is contained in:
2024-10-13 20:14:40 +04:00
parent 2a7e63dce4
commit 8ed9ce17e7
66 changed files with 1486 additions and 1067 deletions

View File

@@ -15,8 +15,27 @@
<option name="projectNumber" value="946974192625" /> <option name="projectNumber" value="946974192625" />
</ConnectionSetting> </ConnectionSetting>
</option> </option>
<option name="devices">
<list>
<DeviceSetting>
<option name="deviceType" value="Phone" />
<option name="displayName" value="Xiaomi (2311DRK48G)" />
<option name="manufacturer" value="Xiaomi" />
<option name="model" value="2311DRK48G" />
</DeviceSetting>
</list>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" /> <option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" /> <option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="versions">
<list>
<VersionSetting>
<option name="buildVersion" value="13" />
<option name="displayName" value="1.7.1 (13)" />
<option name="displayVersion" value="1.7.1" />
</VersionSetting>
</list>
</option>
<option name="visibilityType" value="ALL" /> <option name="visibilityType" value="ALL" />
</InsightsFilterSettings> </InsightsFilterSettings>
</value> </value>

View File

@@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-10T18:25:08.106861775Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=482a22d" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

View File

@@ -33,8 +33,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 13 versionCode = 15
versionName = "1.7.1" versionName = "1.8.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -12,31 +12,17 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- For schedule CLV service-->
<!-- <uses-permission android:name="android.permission.USE_EXACT_ALARM" />-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:name=".PolytechnicApplication" android:name=".PolytechnicApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.PolytechnicNext" android:theme="@style/Theme.PolytechnicNext"
tools:targetApi="35"> tools:targetApi="35">
<receiver
android:name=".receiver.BootCompletedBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.AlarmReceiver" />
<service <service
android:name=".service.MyFirebaseMessagingService" android:name=".service.MyFirebaseMessagingService"
android:exported="false"> android:exported="false">

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,20 +1,13 @@
package ru.n08i40k.polytechnic.next package ru.n08i40k.polytechnic.next
import android.Manifest import android.Manifest
import android.app.AlarmManager
import android.app.Application import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.receiver.AlarmReceiver
import ru.n08i40k.polytechnic.next.utils.or import ru.n08i40k.polytechnic.next.utils.or
import java.util.Calendar
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@@ -34,84 +27,4 @@ class PolytechnicApplication : Application() {
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) || ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) == PackageManager.PERMISSION_GRANTED)
} }
private fun getDate(group: Group): Calendar? {
val javaCalendar = Calendar.getInstance()
val currentMinutes = javaCalendar.get(Calendar.HOUR_OF_DAY) * 60 +
javaCalendar.get(Calendar.MINUTE)
var startDayIdx = javaCalendar.get(Calendar.DAY_OF_WEEK) - 2
println("Current day is $startDayIdx")
val currentDay = group.days[startDayIdx]
if (currentDay != null) {
val firstLesson = currentDay.first
if (firstLesson == null || firstLesson.time.start < currentMinutes) {
println("Current day already started or ended!")
++startDayIdx
}
}
for (dayIdx in startDayIdx..5) {
println("Trying $dayIdx day...")
val day = group.days[dayIdx] ?: continue
println("Day isn't null")
val firstLesson = day.first ?: continue
println("Day isn't empty")
val executeMinutes = (firstLesson.time.start - 15).coerceAtLeast(0)
println("Schedule minutes at $executeMinutes")
return Calendar.getInstance().apply {
set(Calendar.DAY_OF_WEEK, dayIdx + 2) // sunday is first + index from 0
set(Calendar.HOUR_OF_DAY, executeMinutes / 60)
set(Calendar.MINUTE, executeMinutes % 60)
set(Calendar.SECOND, 0)
// set(Calendar.MINUTE, get(Calendar.MINUTE) + 1)
}
}
return null
}
fun scheduleClvService(group: Group) {
// -1 = вс
// 0 = пн
// 1 = вт
// 2 = ср
// 3 = чт
// 4 = пт
// 5 = сб
println("Getting date...")
val date = getDate(group) ?: return
println("Alarm on this week!")
val alarmManager = applicationContext
.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val pendingIntent =
Intent(applicationContext, AlarmReceiver::class.java).let {
PendingIntent.getBroadcast(
applicationContext,
IntentRequestCodes.ALARM_CLV,
it,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
if (alarmManager != null)
println("Alarm manager isn't null.")
alarmManager?.cancel(pendingIntent)
alarmManager?.set(
AlarmManager.RTC_WAKEUP,
date.timeInMillis,
pendingIntent
)
}
} }

View File

@@ -2,30 +2,18 @@ package ru.n08i40k.polytechnic.next.data.schedule.impl
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository { class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
override suspend fun getGroup(): MyResult<Group> { override suspend fun getGroup(): MyResult<Group> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val groupName = runBlocking {
context.settingsDataStore.data.map { settings -> settings.group }.first()
}
if (groupName.isEmpty())
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val response = tryFuture { val response = tryFuture {
ScheduleGet( ScheduleGet(
ScheduleGet.RequestDto(groupName),
context, context,
it, it,
it it
@@ -37,5 +25,4 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
is MyResult.Success -> MyResult.Success(response.data.group) is MyResult.Success -> MyResult.Success(response.data.group)
} }
} }
}
} }

View File

@@ -20,7 +20,6 @@ open class RequestBase(
override fun getHeaders(): MutableMap<String, String> { override fun getHeaders(): MutableMap<String, String> {
val headers = mutableMapOf<String, String>() val headers = mutableMapOf<String, String>()
headers["Content-Type"] = "application/json; charset=utf-8" headers["Content-Type"] = "application/json; charset=utf-8"
headers["version"] = "1"
return headers return headers
} }

View File

@@ -27,3 +27,10 @@ fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
MyResult.Failure(exception) MyResult.Failure(exception)
} }
} }
fun unwrapException(exception: Exception): Throwable {
if (exception is ExecutionException && exception.cause != null)
return exception.cause!!
return exception
}

View File

@@ -26,7 +26,7 @@ open class CachedRequest(
private val listener: Response.Listener<String>, private val listener: Response.Listener<String>,
errorListener: Response.ErrorListener?, errorListener: Response.ErrorListener?,
) : AuthorizedRequest(context, method, url, { ) : AuthorizedRequest(context, method, url, {
runBlocking { runBlocking(Dispatchers.IO) {
(context as PolytechnicApplication) (context as PolytechnicApplication)
.container.networkCacheRepository.put(url, it) .container.networkCacheRepository.put(url, it)
} }

View File

@@ -7,7 +7,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class AuthLogin( class AuthSignIn(
private val data: RequestDto, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<ResponseDto>,
@@ -23,9 +23,16 @@ class AuthLogin(
data class RequestDto(val username: String, val password: String) data class RequestDto(val username: String, val password: String)
@Serializable @Serializable
data class ResponseDto(val id: String, val accessToken: String) data class ResponseDto(val id: String, val accessToken: String, val group: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "2"
return headers
}
} }

View File

@@ -8,7 +8,7 @@ import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class AuthRegister( class AuthSignUp(
private val data: RequestDto, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<ResponseDto>,

View File

@@ -3,13 +3,11 @@ package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.request.CachedRequest import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGet( class ScheduleGet(
private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null errorListener: Response.ErrorListener? = null
@@ -29,8 +27,4 @@ class ScheduleGet(
val group: Group, val group: Group,
val lastChangedDays: ArrayList<Int>, val lastChangedDays: ArrayList<Int>,
) )
override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray()
}
} }

View File

@@ -24,4 +24,11 @@ class ScheduleGetCacheStatus(
val lastCacheUpdate: Long, val lastCacheUpdate: Long,
val lastScheduleUpdate: Long, val lastScheduleUpdate: Long,
) )
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "1"
return headers
}
} }

View File

@@ -4,13 +4,13 @@ import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.request.CachedRequest import ru.n08i40k.polytechnic.next.network.RequestBase
class ScheduleGetGroupNames( class ScheduleGetGroupNames(
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null errorListener: Response.ErrorListener? = null
) : CachedRequest( ) : RequestBase(
context, context,
Method.GET, Method.GET,
"schedule/get-group-names", "schedule/get-group-names",

View File

@@ -25,4 +25,11 @@ class ScheduleUpdate(
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "1"
return headers
}
} }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -3,25 +3,24 @@ package ru.n08i40k.polytechnic.next.service
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.work.OneTimeWorkRequestBuilder import androidx.core.content.ContextCompat.startForegroundService
import androidx.work.WorkManager
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.utils.fmtAsClock import ru.n08i40k.polytechnic.next.utils.fmtAsClock
import ru.n08i40k.polytechnic.next.utils.getDayMinutes import ru.n08i40k.polytechnic.next.utils.getDayMinutes
import ru.n08i40k.polytechnic.next.work.StartClvService
import java.util.Calendar import java.util.Calendar
import java.util.logging.Logger
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
class CurrentLessonViewService : Service() { class CurrentLessonViewService : Service() {
@@ -30,19 +29,31 @@ class CurrentLessonViewService : Service() {
private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1 private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
private const val UPDATE_INTERVAL = 60_000L private const val UPDATE_INTERVAL = 60_000L
fun startService(appContext: Context) { suspend fun startService(application: PolytechnicApplication) {
if (!(appContext as PolytechnicApplication).hasNotificationPermission()) if (!application.hasNotificationPermission())
return return
if (Calendar.getInstance() val schedule =
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance() application
.get(Calendar.MINUTE) < 420) .container
.scheduleRepository
.getGroup()
if (schedule is MyResult.Failure)
return return
val request = OneTimeWorkRequestBuilder<StartClvService>() val intent = Intent(application, CurrentLessonViewService::class.java)
.build() .apply {
putExtra("group", (schedule as MyResult.Success).data)
}
WorkManager.getInstance(appContext).enqueue(request) application.stopService(
Intent(
application,
CurrentLessonViewService::class.java
)
)
startForegroundService(application, intent)
} }
} }
@@ -51,7 +62,10 @@ class CurrentLessonViewService : Service() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val updateRunnable = object : Runnable { private val updateRunnable = object : Runnable {
override fun run() { override fun run() {
val logger = Logger.getLogger("CLV.updateRunnable")
if (day == null || day!!.nonNullIndices.isEmpty()) { if (day == null || day!!.nonNullIndices.isEmpty()) {
logger.warning("Stopping, because day is null or empty!")
stopSelf() stopSelf()
return return
} }
@@ -74,7 +88,7 @@ class CurrentLessonViewService : Service() {
if (currentLesson == null && nextLesson == null) { if (currentLesson == null && nextLesson == null) {
val notification = NotificationCompat val notification = NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW) .Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.logo) .setSmallIcon(R.drawable.schedule)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(getString(R.string.lessons_end_notification_title)) .setContentTitle(getString(R.string.lessons_end_notification_title))
.setContentText(getString(R.string.lessons_end_notification_description)) .setContentText(getString(R.string.lessons_end_notification_description))
@@ -146,7 +160,7 @@ class CurrentLessonViewService : Service() {
): Notification { ): Notification {
return NotificationCompat return NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW) .Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.logo) .setSmallIcon(R.drawable.schedule)
.setContentTitle(title ?: getString(R.string.lesson_notification_title)) .setContentTitle(title ?: getString(R.string.lesson_notification_title))
.setContentText(description ?: getString(R.string.lesson_notification_description)) .setContentText(description ?: getString(R.string.lesson_notification_description))
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
@@ -159,26 +173,29 @@ class CurrentLessonViewService : Service() {
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
} }
private fun updateSchedule(group: Group?): Boolean { private fun updateSchedule(group: Group?) {
val logger = Logger.getLogger("CLV")
if (group == null) { if (group == null) {
logger.warning("Stopping, because group is null")
stopSelf() stopSelf()
return false return
} }
val currentDay = group.current val currentDay = group.current
if (currentDay == null || currentDay.nonNullIndices.isEmpty()) { if (currentDay == null || currentDay.nonNullIndices.isEmpty()) {
logger.warning("Stopping, because current day is null or empty")
stopSelf() stopSelf()
return false return
} }
if (this.day == null) { val nowMinutes = Calendar.getInstance().getDayMinutes()
val nowMinutes = Calendar.getInstance().getDayMinutes() if (nowMinutes < ((5 * 60) + 30)
|| currentDay.last!!.time.end < nowMinutes
if (currentDay.first!!.time.start - nowMinutes > 30 ) {
|| currentDay.last!!.time.end < nowMinutes) { logger.warning("Stopping, because service started outside of acceptable time range!")
stopSelf() stopSelf()
return false return
}
} }
this.day = currentDay this.day = currentDay
@@ -186,7 +203,7 @@ class CurrentLessonViewService : Service() {
this.handler.removeCallbacks(updateRunnable) this.handler.removeCallbacks(updateRunnable)
updateRunnable.run() updateRunnable.run()
return true logger.info("Running...")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -201,16 +218,14 @@ class CurrentLessonViewService : Service() {
val notification = createNotification() val notification = createNotification()
startForeground(NOTIFICATION_STATUS_ID, notification) startForeground(NOTIFICATION_STATUS_ID, notification)
if (!updateSchedule( updateSchedule(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("group", Group::class.java) intent.getParcelableExtra("group", Group::class.java)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
intent.getParcelableExtra("group") intent.getParcelableExtra("group")
} }
)
) )
updateRunnable.run()
return START_STICKY return START_STICKY
} }

View File

@@ -17,13 +17,19 @@ import androidx.work.WorkManager
import androidx.work.workDataOf import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
import ru.n08i40k.polytechnic.next.work.ScheduleClvAlarm
import java.time.Duration import java.time.Duration
class MyFirebaseMessagingService : FirebaseMessagingService() { class MyFirebaseMessagingService : FirebaseMessagingService() {
val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
super.onNewToken(token) super.onNewToken(token)
@@ -79,7 +85,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
} }
notify(id.hashCode(), notification) notify(id.hashCode(), notification)
CurrentLessonViewService.startService(applicationContext)
} }
} }
@@ -90,7 +95,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
"schedule-update" -> { "schedule-update" -> {
sendNotification( sendNotification(
NotificationChannels.SCHEDULE_UPDATE, NotificationChannels.SCHEDULE_UPDATE,
R.drawable.logo, R.drawable.schedule,
getString(R.string.schedule_update_title), getString(R.string.schedule_update_title),
getString( getString(
if (message.data["replaced"] == "true") if (message.data["replaced"] == "true")
@@ -101,24 +106,19 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
NotificationCompat.PRIORITY_DEFAULT, NotificationCompat.PRIORITY_DEFAULT,
message.data["etag"] message.data["etag"]
) )
}
val constraints = Constraints.Builder() "lessons-start" -> {
.setRequiredNetworkType(NetworkType.CONNECTED) scope.launch {
.build() CurrentLessonViewService
.startService(applicationContext as PolytechnicApplication)
val request = OneTimeWorkRequestBuilder<ScheduleClvAlarm>() }
.setConstraints(constraints)
.build()
WorkManager
.getInstance(applicationContext)
.enqueue(request)
} }
"app-update" -> { "app-update" -> {
sendNotification( sendNotification(
NotificationChannels.APP_UPDATE, NotificationChannels.APP_UPDATE,
R.drawable.logo, R.drawable.download,
getString(R.string.app_update_title, message.data["version"]), getString(R.string.app_update_title, message.data["version"]),
getString(R.string.app_update_description), getString(R.string.app_update_description),
NotificationCompat.PRIORITY_DEFAULT, NotificationCompat.PRIORITY_DEFAULT,

View File

@@ -36,7 +36,6 @@ import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
@@ -101,21 +100,6 @@ class MainActivity : ComponentActivity() {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} }
private fun scheduleAlarm() {
lifecycleScope.launch {
val schedule = (applicationContext as PolytechnicApplication)
.container
.scheduleRepository
.getGroup()
if (schedule is MyResult.Failure)
return@launch
(applicationContext as PolytechnicApplication)
.scheduleClvService((schedule as MyResult.Success).data)
}
}
fun scheduleLinkUpdate(intervalInMinutes: Long) { fun scheduleLinkUpdate(intervalInMinutes: Long) {
val tag = "schedule-update" val tag = "schedule-update"
@@ -182,7 +166,6 @@ class MainActivity : ComponentActivity() {
setupFirebaseConfig() setupFirebaseConfig()
handleUpdate() handleUpdate()
scheduleAlarm()
setContent { setContent {
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) { Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {

View File

@@ -1,504 +1,113 @@
package ru.n08i40k.polytechnic.next.ui.auth package ru.n08i40k.polytechnic.next.ui.auth
import android.content.Context import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.android.volley.AuthFailureError
import com.android.volley.ClientError
import com.android.volley.TimeoutError
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.model.UserRole.Companion.AcceptableUserRoles
import ru.n08i40k.polytechnic.next.network.request.auth.AuthLogin
import ru.n08i40k.polytechnic.next.network.request.auth.AuthRegister
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
@Preview(showBackground = true)
@Composable
private fun LoginForm(
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
navController: NavHostController = rememberNavController(),
scope: CoroutineScope = rememberCoroutineScope(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val mutableIsLoading = remember { mutableStateOf(false) }
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var visible by mutableVisible
Text(
text = stringResource(R.string.login_title),
modifier = Modifier.padding(10.dp),
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.ExtraBold
)
Spacer(modifier = Modifier.size(10.dp))
val mutableUsernameError = remember { mutableStateOf(false) }
val mutablePasswordError = remember { mutableStateOf(false) }
var usernameError by mutableUsernameError
var passwordError by mutablePasswordError
OutlinedTextField(
value = username,
singleLine = true,
onValueChange = {
username = it
usernameError = false
},
label = { Text(stringResource(R.string.username)) },
isError = usernameError
)
OutlinedTextField(
value = password,
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
onValueChange = {
passwordError = false
password = it
},
label = { Text(stringResource(R.string.password)) },
isError = passwordError
)
TextButton(onClick = { visible = false }) {
Text(text = stringResource(R.string.not_registered))
}
Button(
enabled = !mutableIsLoading.value,
onClick = {
if (username.length < 4) usernameError = true
if (password.isEmpty()) passwordError = true
if (usernameError || passwordError) return@Button
tryLogin(
username,
password,
mutableUsernameError,
mutablePasswordError,
mutableIsLoading,
context,
snackbarHostState,
scope,
navController
)
focusManager.clearFocus()
}
) {
Text(
text = stringResource(R.string.login),
style = MaterialTheme.typography.bodyLarge
)
}
}
@Preview(showBackground = true)
@Composable
private fun RegisterForm(
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
navController: NavHostController = rememberNavController(),
scope: CoroutineScope = rememberCoroutineScope(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val mutableIsLoading = remember { mutableStateOf(false) }
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var group by remember { mutableStateOf("") }
val mutableRole = remember { mutableStateOf(UserRole.STUDENT) }
var visible by mutableVisible
Text(
text = stringResource(R.string.register_title),
modifier = Modifier.padding(10.dp),
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.ExtraBold
)
Spacer(modifier = Modifier.size(10.dp))
val mutableUsernameError = remember { mutableStateOf(false) }
var usernameError by mutableUsernameError
var passwordError by remember { mutableStateOf(false) }
val mutableGroupError = remember { mutableStateOf(false) }
var groupError by mutableGroupError
OutlinedTextField(
value = username,
singleLine = true,
onValueChange = {
username = it
usernameError = false
},
label = { Text(stringResource(R.string.username)) },
isError = usernameError
)
OutlinedTextField(
value = password,
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
onValueChange = {
passwordError = false
password = it
},
label = { Text(stringResource(R.string.password)) },
isError = passwordError
)
OutlinedTextField(
value = group,
singleLine = true,
onValueChange = {
groupError = false
group = it
},
label = { Text(stringResource(R.string.group)) },
isError = groupError
)
RoleSelector(mutableRole)
TextButton(onClick = { visible = false }) {
Text(text = stringResource(R.string.already_registered))
}
Button(
enabled = !mutableIsLoading.value,
onClick = {
if (username.length < 4) usernameError = true
if (password.isEmpty()) passwordError = true
if (group.isEmpty()) groupError = true
if (usernameError || passwordError || groupError) return@Button
tryRegister(
username,
password,
group,
mutableRole.value,
mutableUsernameError,
mutableGroupError,
mutableIsLoading,
context,
snackbarHostState,
scope,
navController
)
focusManager.clearFocus()
}
) {
Text(
text = stringResource(R.string.register),
style = MaterialTheme.typography.bodyLarge
)
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun AuthForm( fun AuthForm(
mutableIsLogin: MutableState<Boolean> = mutableStateOf(true), appNavController: NavHostController = rememberNavController(),
navController: NavHostController = rememberNavController(), onPendingSnackbar: (String) -> Unit = {},
scope: CoroutineScope = rememberCoroutineScope(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
) { ) {
var isLogin by mutableIsLogin val navController = rememberNavController()
val mutableVisible = remember { mutableStateOf(true) } val modifier = Modifier.fillMaxSize()
var visible by mutableVisible
val animatedAlpha by animateFloatAsState( NavHost(
targetValue = if (visible) 1.0f else 0f, label = "alpha" modifier = Modifier.fillMaxSize(),
) navController = navController,
startDestination = "sign-in",
Column( enterTransition = {
modifier = Modifier slideIn(
.padding(10.dp) animationSpec = tween(
.graphicsLayer { 400,
alpha = animatedAlpha delayMillis = 250,
if (alpha == 0F) { easing = LinearOutSlowInEasing
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!"
) )
) { 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) @Preview(showBackground = true)
@Composable @Composable
fun AuthScreen(navController: NavHostController = rememberNavController()) { fun AuthScreen(appNavController: NavHostController = rememberNavController()) {
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -506,14 +115,20 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
context.settingsDataStore.data.map { settings -> settings.accessToken }.first() context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
} }
if (accessToken.isNotEmpty()) navController.navigate("main") if (accessToken.isNotEmpty()) {
appNavController.navigate("main") {
popUpTo("auth") { inclusive = true }
}
}
} }
val mutableIsLogin = remember { mutableStateOf(true) }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val onPendingSnackbar: (String) -> Unit = {
scope.launch { snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Long) }
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp), contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
@@ -525,14 +140,10 @@ fun AuthScreen(navController: NavHostController = rememberNavController()) {
horizontalArrangement = Arrangement.SpaceAround, horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Card { AuthForm(
AuthForm( appNavController,
mutableIsLogin, onPendingSnackbar
navController, )
scope,
snackbarHostState
)
}
} }
}) })
} }

View File

@@ -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
)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -6,4 +6,4 @@ object FilledGroup
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")
val AppIcons.Filled: FilledGroup val AppIcons.Filled: FilledGroup
get() = FilledGroup get() = FilledGroup

View File

@@ -1,3 +1,5 @@
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
@Suppress("UnusedReceiverParameter")
val FilledGroup.Download: ImageVector val FilledGroup.Download: ImageVector
get() { get() {
if (_download != null) { if (_download != null) {
return _download!! return _download!!
} }
_download = Builder( _download = Builder(
name = "Download", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, name = "Download", defaultWidth = 24.dp, defaultHeight = 24.dp,
viewportWidth = 24.0f, viewportHeight = 24.0f viewportWidth = 24.0f, viewportHeight = 24.0f
).apply { ).apply {
path( path(
@@ -55,5 +56,4 @@ val FilledGroup.Download: ImageVector
return _download!! return _download!!
} }
@Suppress("ObjectPropertyName")
private var _download: ImageVector? = null private var _download: ImageVector? = null

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,5 @@
@file:Suppress("ObjectPropertyName", "UnusedReceiverParameter")
package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -11,14 +13,13 @@ import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
@Suppress("UnusedReceiverParameter")
val FilledGroup.Telegram: ImageVector val FilledGroup.Telegram: ImageVector
get() { get() {
if (_telegram != null) { if (_telegram != null) {
return _telegram!! return _telegram!!
} }
_telegram = Builder( _telegram = Builder(
name = "Telegram", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, name = "Telegram", defaultWidth = 24.dp, defaultHeight = 24.dp,
viewportWidth = 24.0f, viewportHeight = 24.0f viewportWidth = 24.0f, viewportHeight = 24.0f
).apply { ).apply {
path( path(
@@ -49,5 +50,4 @@ val FilledGroup.Telegram: ImageVector
return _telegram!! return _telegram!!
} }
@Suppress("ObjectPropertyName")
private var _telegram: ImageVector? = null private var _telegram: ImageVector? = null

View File

@@ -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

View File

@@ -116,21 +116,21 @@ private fun NavHostContainer(
) )
) )
}, },
builder = { ) {
composable("profile") { composable("profile") {
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() } ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
} }
composable("schedule") { composable("schedule") {
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() } ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
} }
if (scheduleReplacerViewModel != null) { if (scheduleReplacerViewModel != null) {
composable("replacer") { composable("replacer") {
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() } ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
}
} }
}) }
}
} }
private fun openLink(context: Context, link: String) { private fun openLink(context: Context, link: String) {
@@ -260,7 +260,11 @@ fun MainScreen(
viewModel( viewModel(
factory = ProfileViewModel.provideFactory( factory = ProfileViewModel.provideFactory(
profileRepository = mainViewModel.appContainer.profileRepository, profileRepository = mainViewModel.appContainer.profileRepository,
onUnauthorized = { appNavController.navigate("auth") }) onUnauthorized = {
appNavController.navigate("auth") {
popUpTo("main") { inclusive = true }
}
})
) )
LocalContext.current.profileViewModel = profileViewModel LocalContext.current.profileViewModel = profileViewModel

View File

@@ -1,26 +1,14 @@
package ru.n08i40k.polytechnic.next.ui.main.profile package ru.n08i40k.polytechnic.next.ui.main.profile
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -36,7 +24,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
private enum class ChangeGroupError { private enum class ChangeGroupError {
NOT_EXISTS NOT_EXISTS
@@ -57,80 +45,6 @@ private fun tryChangeGroup(
}).send() }).send()
} }
@Composable
private fun getGroups(context: Context): ArrayList<String> {
val groupPlaceholder = stringResource(R.string.loading)
val groups = remember { arrayListOf(groupPlaceholder) }
LaunchedEffect(groups) {
ScheduleGetGroupNames(context, {
groups.clear()
groups.addAll(it.names)
}, {
throw it
}).send()
}
return groups
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun GroupSelector(
value: String = "ИС-214/24",
onValueChange: (String) -> Unit = {},
isError: Boolean = false,
readOnly: Boolean = false,
) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize()
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !readOnly && !expanded
}
) {
TextField(
label = { Text(stringResource(R.string.group)) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
value = value,
leadingIcon = {
Icon(
imageVector = Icons.Filled.Email,
contentDescription = "group"
)
},
onValueChange = {},
isError = isError,
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
)
val context = LocalContext.current
val groups = getGroups(context)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }) {
groups.forEach {
DropdownMenuItem(
text = { Text(it) },
onClick = {
onValueChange(it)
expanded = false
}
)
}
}
}
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
internal fun ChangeGroupDialog( internal fun ChangeGroupDialog(
@@ -141,7 +55,7 @@ internal fun ChangeGroupDialog(
) { ) {
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
Card { Card {
var group by remember { mutableStateOf("ИС-214/23") } var group by remember { mutableStateOf<String?>(profile.group) }
var groupError by remember { mutableStateOf(false) } var groupError by remember { mutableStateOf(false) }
var processing by remember { mutableStateOf(false) } var processing by remember { mutableStateOf(false) }
@@ -165,7 +79,7 @@ internal fun ChangeGroupDialog(
tryChangeGroup( tryChangeGroup(
context = context, context = context,
group = group, group = group!!,
onError = { onError = {
when (it) { when (it) {
ChangeGroupError.NOT_EXISTS -> { ChangeGroupError.NOT_EXISTS -> {
@@ -178,7 +92,7 @@ internal fun ChangeGroupDialog(
onSuccess = onChange onSuccess = onChange
) )
}, },
enabled = !(groupError || processing) enabled = !(groupError || processing) && group != null
) { ) {
Text(stringResource(R.string.change_group)) Text(stringResource(R.string.change_group))
} }

View File

@@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
@@ -187,6 +188,10 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
context.settingsDataStore.updateData { context.settingsDataStore.updateData {
it.toBuilder().setGroup(group).build() it.toBuilder().setGroup(group).build()
} }
(context.applicationContext as PolytechnicApplication)
.container
.networkCacheRepository
.clear()
} }
context.profileViewModel!!.refreshProfile { context.profileViewModel!!.refreshProfile {
scheduleViewModel.refreshGroup() scheduleViewModel.refreshGroup()

View File

@@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel

View File

@@ -43,7 +43,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel

View File

@@ -33,23 +33,15 @@ import kotlinx.coroutines.flow.flow
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonType import ru.n08i40k.polytechnic.next.model.LessonType
import java.util.Calendar
private fun getCurrentMinutes(): Int {
return Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
}
@Composable @Composable
private fun getMinutes(): Flow<Int> { private fun getCurrentLessonIdx(day: Day?): Flow<Int> {
val value by remember { val value by remember {
derivedStateOf { derivedStateOf {
flow { flow {
while (true) { while (true) {
emit(getCurrentMinutes()) emit(day?.currentIdx ?: -1)
delay(5_000) delay(5_000)
} }
} }
@@ -59,22 +51,6 @@ private fun getMinutes(): Flow<Int> {
return value return value
} }
@Composable
fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
val currentMinutes by getMinutes().collectAsStateWithLifecycle(0)
val filteredLessons = lessons
.filterNotNull()
.filter {
it.time.start <= currentMinutes && it.time.end >= currentMinutes
}
if (filteredLessons.isEmpty())
return -1
return lessons.indexOf(filteredLessons[0])
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun DayCard( fun DayCard(
@@ -99,8 +75,8 @@ fun DayCard(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = containerColor =
if (current) MaterialTheme.colorScheme.inverseSurface if (current) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.secondaryContainer
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface) border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
) { ) {
@@ -109,14 +85,10 @@ fun DayCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
text = stringResource(R.string.day_null), text = stringResource(R.string.day_null)
color =
if (current) MaterialTheme.colorScheme.inverseOnSurface
else MaterialTheme.colorScheme.onSurface
) )
return@Card return@Card
} }
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -124,7 +96,8 @@ fun DayCard(
text = day.name, text = day.name,
) )
val currentLessonIdx = calculateCurrentLessonIdx(day.lessons) val currentLessonIdx by getCurrentLessonIdx(if (current) day else null)
.collectAsStateWithLifecycle(0)
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View File

@@ -1,21 +1,50 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp import androidx.compose.ui.util.lerp
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.temporal.WeekFields
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import java.util.logging.Level
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
private fun isCurrentWeek(group: Group): Boolean {
if (group.days.size == 0 || group.days[0] == null)
return true
val dateString = group.days[0]!!.name
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale("ru"))
val datePart = dateString.split(" ").getOrNull(1) ?: return true
val date = LocalDate.parse(datePart, formatter)
val currentDate = LocalDate.now()
val weekField = WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear()
val currentWeek = currentDate.get(weekField)
val dateWeek = date.get(weekField)
return dateWeek >= currentWeek
}
@Preview @Preview
@Composable @Composable
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) { fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
@@ -23,31 +52,38 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
val calendarDay = currentDay val calendarDay = currentDay
.coerceAtLeast(0) .coerceAtLeast(0)
.coerceAtMost(group.days.size - 1) .coerceAtMost(group.days.size - 1)
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size }) val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size })
HorizontalPager( Column {
state = pagerState, if (!isCurrentWeek(group)) {
contentPadding = PaddingValues(horizontal = 20.dp), NotificationCard(
verticalAlignment = Alignment.Top, level = Level.WARNING,
modifier = Modifier.height(600.dp) title = stringResource(R.string.outdated_schedule)
) { page -> )
DayCard( }
modifier = Modifier.graphicsLayer { HorizontalPager(
val offset = pagerState.getOffsetDistanceInPages(page).absoluteValue 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( lerp(
start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f) start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f)
).also { scale -> ).also { scale ->
scaleX = scale scaleX = scale
scaleY = scale scaleY = scale
} }
alpha = lerp( alpha = lerp(
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f) start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
) )
}, },
day = group.days[page], day = group.days[page],
current = currentDay == page current = currentDay == page
) )
}
} }
} }

View File

@@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel

View File

@@ -19,7 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.ui.ExpandableCard import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCard
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCardTitle
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -49,7 +50,7 @@ fun UpdateInfo(
ExpandableCard( ExpandableCard(
expanded = expanded, expanded = expanded,
onExpandedChange = { expanded = !expanded }, onExpandedChange = { expanded = !expanded },
title = stringResource(R.string.update_info_header) title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) }
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier

View File

@@ -1,5 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.theme package ru.n08i40k.polytechnic.next.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF4C662B) val primaryLight = Color(0xFF4C662B)
@@ -218,9 +217,33 @@ val surfaceContainerDarkHighContrast = Color(0xFF1E201A)
val surfaceContainerHighDarkHighContrast = Color(0xFF282B24) val surfaceContainerHighDarkHighContrast = Color(0xFF282B24)
val surfaceContainerHighestDarkHighContrast = Color(0xFF33362E) val surfaceContainerHighestDarkHighContrast = Color(0xFF33362E)
val warningLight = Color(0xFF7B580C)
val onWarningLight = Color(0xFFFFFFFF)
val warningContainerLight = Color(0xFFFFDEA8)
val onWarningContainerLight = Color(0xFF271900)
val warningLightMediumContrast = Color(0xFF593E00)
val onWarningLightMediumContrast = Color(0xFFFFFFFF)
val warningContainerLightMediumContrast = Color(0xFF946E24)
val onWarningContainerLightMediumContrast = Color(0xFFFFFFFF)
val warningLightHighContrast = Color(0xFF2F1F00)
val onWarningLightHighContrast = Color(0xFFFFFFFF)
val warningContainerLightHighContrast = Color(0xFF593E00)
val onWarningContainerLightHighContrast = Color(0xFFFFFFFF)
val warningDark = Color(0xFFEEBF6D)
val onWarningDark = Color(0xFF422D00)
val warningContainerDark = Color(0xFF5E4200)
val onWarningContainerDark = Color(0xFFFFDEA8)
val warningDarkMediumContrast = Color(0xFFF2C470)
val onWarningDarkMediumContrast = Color(0xFF201400)
val warningContainerDarkMediumContrast = Color(0xFFB38A3D)
val onWarningContainerDarkMediumContrast = Color(0xFF000000)
val warningDarkHighContrast = Color(0xFFFFFAF7)
val onWarningDarkHighContrast = Color(0xFF000000)
val warningContainerDarkHighContrast = Color(0xFFF2C470)
val onWarningContainerDarkHighContrast = Color(0xFF000000)

View File

@@ -14,6 +14,11 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@Immutable
data class ExtendedColorScheme(
val warning: ColorFamily,
)
private val lightScheme = lightColorScheme( private val lightScheme = lightColorScheme(
primary = primaryLight, primary = primaryLight,
onPrimary = onPrimaryLight, onPrimary = onPrimaryLight,
@@ -242,6 +247,60 @@ private val highContrastDarkColorScheme = darkColorScheme(
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
) )
val extendedLight = ExtendedColorScheme(
warning = ColorFamily(
warningLight,
onWarningLight,
warningContainerLight,
onWarningContainerLight,
),
)
val extendedDark = ExtendedColorScheme(
warning = ColorFamily(
warningDark,
onWarningDark,
warningContainerDark,
onWarningContainerDark,
),
)
val extendedLightMediumContrast = ExtendedColorScheme(
warning = ColorFamily(
warningLightMediumContrast,
onWarningLightMediumContrast,
warningContainerLightMediumContrast,
onWarningContainerLightMediumContrast,
),
)
val extendedLightHighContrast = ExtendedColorScheme(
warning = ColorFamily(
warningLightHighContrast,
onWarningLightHighContrast,
warningContainerLightHighContrast,
onWarningContainerLightHighContrast,
),
)
val extendedDarkMediumContrast = ExtendedColorScheme(
warning = ColorFamily(
warningDarkMediumContrast,
onWarningDarkMediumContrast,
warningContainerDarkMediumContrast,
onWarningContainerDarkMediumContrast,
),
)
val extendedDarkHighContrast = ExtendedColorScheme(
warning = ColorFamily(
warningDarkHighContrast,
onWarningDarkHighContrast,
warningContainerDarkHighContrast,
onWarningContainerDarkHighContrast,
),
)
@Immutable @Immutable
data class ColorFamily( data class ColorFamily(
val color: Color, val color: Color,
@@ -254,12 +313,17 @@ val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
) )
@Composable
fun extendedColorScheme(): ExtendedColorScheme {
return if (isSystemInDarkTheme()) extendedDark else extendedLight
}
@Composable @Composable
fun AppTheme( fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable() () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui package ru.n08i40k.polytechnic.next.ui.widgets
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
@@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -39,9 +38,14 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun ExpandableCard( fun ExpandableCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
expanded: Boolean, colors: CardColors = CardDefaults.cardColors(),
border: BorderStroke = BorderStroke(
Dp.Hairline,
MaterialTheme.colorScheme.inverseSurface
),
expanded: Boolean = false,
onExpandedChange: () -> Unit, onExpandedChange: () -> Unit,
title: String, title: @Composable () -> Unit,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val transitionState = remember { val transitionState = remember {
@@ -57,8 +61,8 @@ fun ExpandableCard(
onExpandedChange() onExpandedChange()
transitionState.targetState = expanded transitionState.targetState = expanded
}, },
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), colors = colors,
border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface) border = border
) { ) {
Column { Column {
ExpandableCardHeader(title, transition) ExpandableCardHeader(title, transition)
@@ -67,6 +71,25 @@ fun ExpandableCard(
} }
} }
@Composable
fun ExpandableCard(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
colors: CardColors = CardDefaults.cardColors(),
border: BorderStroke = BorderStroke(
Dp.Hairline,
MaterialTheme.colorScheme.inverseSurface
),
) {
Card(
modifier = modifier,
colors = colors,
border = border
) {
ExpandableCardHeader(title, null)
}
}
@Composable @Composable
private fun ExpandableCardContent( private fun ExpandableCardContent(
visible: Boolean = true, visible: Boolean = true,
@@ -99,7 +122,7 @@ private fun ExpandableCardContent(
} }
@Composable @Composable
private fun ExpandableCardTitle(text: String) { fun ExpandableCardTitle(text: String) {
Text( Text(
text = text, text = text,
modifier = Modifier modifier = Modifier
@@ -130,8 +153,8 @@ private fun ExpandableCardArrow(
@Composable @Composable
private fun ExpandableCardHeader( private fun ExpandableCardHeader(
title: String = "TODO", title: @Composable () -> Unit,
transition: Transition<Boolean> transition: Transition<Boolean>?
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -139,7 +162,8 @@ private fun ExpandableCardHeader(
.padding(10.dp, 0.dp), .padding(10.dp, 0.dp),
contentAlignment = Alignment.CenterEnd, contentAlignment = Alignment.CenterEnd,
) { ) {
ExpandableCardArrow(transition) if (transition != null)
ExpandableCardTitle(title) ExpandableCardArrow(transition)
title()
} }
} }

View File

@@ -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
}
)
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui package ru.n08i40k.polytechnic.next.ui.widgets
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box

View File

@@ -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
)
}
}

View File

@@ -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)
}
)
}
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -3,10 +3,9 @@
<string name="app_name">Политехникум</string> <string name="app_name">Политехникум</string>
<string name="username">Имя пользователя</string> <string name="username">Имя пользователя</string>
<string name="password">Пароль</string> <string name="password">Пароль</string>
<string name="register">Зарегистрироваться</string> <string name="proceed">Продолжить</string>
<string name="login">Авторизоваться</string> <string name="sign_in_title">Авторизация</string>
<string name="login_title">Авторизация</string> <string name="sign_up_title">Регистрация</string>
<string name="register_title">Регистрация</string>
<string name="not_registered">Не зарегистрированы?</string> <string name="not_registered">Не зарегистрированы?</string>
<string name="already_registered">Уже зарегистрированы?</string> <string name="already_registered">Уже зарегистрированы?</string>
<string name="reload">Перезагрузить</string> <string name="reload">Перезагрузить</string>
@@ -23,7 +22,7 @@
<string name="role_admin">Администратор</string> <string name="role_admin">Администратор</string>
<string name="group">Группа</string> <string name="group">Группа</string>
<string name="role">Роль</string> <string name="role">Роль</string>
<string name="day_null">Расписание ещё не обновилось.</string> <string name="day_null">На этот день расписания ещё нет!</string>
<string name="old_password">Старый пароль</string> <string name="old_password">Старый пароль</string>
<string name="new_password">Новый пароль</string> <string name="new_password">Новый пароль</string>
<string name="loading">Загрузка…</string> <string name="loading">Загрузка…</string>
@@ -65,4 +64,13 @@
<string name="in_gym_lc">в спорт-зале</string> <string name="in_gym_lc">в спорт-зале</string>
<string name="lessons_not_started">Пары ещё не начались</string> <string name="lessons_not_started">Пары ещё не начались</string>
<string name="waiting_for_day_start_notification_title">До начала пар %1$d ч. %2$d мин.</string> <string name="waiting_for_day_start_notification_title">До начала пар %1$d ч. %2$d мин.</string>
<string name="outdated_schedule">Вы просматриваете устаревшее расписание!</string>
<string name="invalid_credentials">Некорректное имя пользователя или пароль!</string>
<string name="timed_out">Неудалось отправить запрос, попробуйте позже!</string>
<string name="app_too_old">Пожалуйста обновите приложение!</string>
<string name="unknown_error">Произошла неизвестная ошибка! Попробуйте позже.</string>
<string name="failed_to_fetch_group_names">Не удалось получить список названий групп!</string>
<string name="already_exists">Пользователь с таким именем уже зарегистрирован!</string>
<string name="group_does_not_exists">Группа с таким названием не существует!</string>
<string name="no_connection">Нет подключения к интернету!</string>
</resources> </resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -3,10 +3,9 @@
<string name="username">Username</string> <string name="username">Username</string>
<string name="password">Password</string> <string name="password">Password</string>
<string name="register">Register</string> <string name="proceed">Proceed</string>
<string name="login">Login</string> <string name="sign_in_title">Sign In</string>
<string name="login_title">Login</string> <string name="sign_up_title">Sign Up</string>
<string name="register_title">Registration</string>
<string name="not_registered">Not registered?</string> <string name="not_registered">Not registered?</string>
<string name="already_registered">Already registered?</string> <string name="already_registered">Already registered?</string>
<string name="reload">Reload</string> <string name="reload">Reload</string>
@@ -23,7 +22,7 @@
<string name="role_admin">Administrator</string> <string name="role_admin">Administrator</string>
<string name="group">Group</string> <string name="group">Group</string>
<string name="role">Role</string> <string name="role">Role</string>
<string name="day_null">Schedule not updated yet.</string> <string name="day_null">There is no schedule for this day yet!</string>
<string name="old_password">Old password</string> <string name="old_password">Old password</string>
<string name="new_password">New password</string> <string name="new_password">New password</string>
<string name="change_password">Change password</string> <string name="change_password">Change password</string>
@@ -65,4 +64,13 @@
<string name="in_gym_lc">in gym</string> <string name="in_gym_lc">in gym</string>
<string name="lessons_not_started">Lessons haven\'t started yet</string> <string name="lessons_not_started">Lessons haven\'t started yet</string>
<string name="waiting_for_day_start_notification_title">%1$d h. %2$d min. before lessons start</string> <string name="waiting_for_day_start_notification_title">%1$d h. %2$d min. before lessons start</string>
<string name="outdated_schedule">You are viewing an outdated schedule!</string>
<string name="invalid_credentials">Invalid credentials!</string>
<string name="timed_out">Failed to send request, try again later!</string>
<string name="app_too_old">Please update the application!</string>
<string name="unknown_error">An unknown error has occurred! Please try again later.</string>
<string name="failed_to_fetch_group_names">Failed to get list of group names!</string>
<string name="already_exists">A user with this name is already registered!</string>
<string name="group_does_not_exists">A group with this name does not exist!</string>
<string name="no_connection">No internet connection!</string>
</resources> </resources>