mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
1.5.0
Уведомления об обновлении расписания. Обновление кеша сервера в фоновом режиме (раз в 15 минут). Теперь полный порядок с запросами.
This commit is contained in:
@@ -32,8 +32,8 @@ android {
|
||||
applicationId = "ru.n08i40k.polytechnic.next"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 9
|
||||
versionName = "1.4.0"
|
||||
versionCode = 10
|
||||
versionName = "1.5.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -89,13 +89,22 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// work manager
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
|
||||
// hilt
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
// firebase
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.messaging)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.protobuf.lite)
|
||||
|
||||
@@ -112,7 +121,7 @@ dependencies {
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.volley)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".PolytechnicApplication"
|
||||
@@ -14,6 +15,15 @@
|
||||
android:roundIcon="@drawable/ic_launcher_round"
|
||||
android:theme="@style/Theme.PolytechnicNext"
|
||||
tools:targetApi="35">
|
||||
|
||||
<service
|
||||
android:name=".service.MyFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
object NotificationChannels {
|
||||
const val SCHEDULE_UPDATE = "schedule-update"
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
|
||||
import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
|
||||
import ru.n08i40k.polytechnic.next.data.cache.impl.LocalNetworkCacheRepository
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.RemoteScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
|
||||
|
||||
@@ -9,14 +9,11 @@ import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReq
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReqData
|
||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
|
||||
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
|
||||
|
||||
|
||||
override suspend fun getGroup(): MyResult<Group> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val groupName = runBlocking {
|
||||
@@ -27,8 +24,8 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
|
||||
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
|
||||
|
||||
val response = tryFuture {
|
||||
ScheduleGetReq(
|
||||
ScheduleGetReqData(groupName),
|
||||
ScheduleGet(
|
||||
ScheduleGet.RequestDto(groupName),
|
||||
context,
|
||||
it,
|
||||
it
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.n08i40k.polytechnic.next.data.schedule.impl
|
||||
package ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl
|
||||
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.n08i40k.polytechnic.next.data.schedule.impl
|
||||
package ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -6,15 +6,15 @@ import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerClearReq
|
||||
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerGetReq
|
||||
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerSetReq
|
||||
import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerClear
|
||||
import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerGet
|
||||
import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerSet
|
||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||
|
||||
class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository {
|
||||
override suspend fun getAll(): MyResult<List<ScheduleReplacer>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
tryFuture { ScheduleReplacerGetReq(context, it, it) }
|
||||
tryFuture { ScheduleReplacerGet(context, it, it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleR
|
||||
fileType: String
|
||||
): MyResult<Nothing> =
|
||||
withContext(Dispatchers.IO) {
|
||||
tryFuture { ScheduleReplacerSetReq(context, fileName, fileData, fileType, it, it) }
|
||||
tryFuture { ScheduleReplacerSet(context, fileName, fileData, fileType, it, it) }
|
||||
}
|
||||
|
||||
override suspend fun clear(): MyResult<Int> {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
tryFuture { ScheduleReplacerClearReq(context, it, it) }
|
||||
tryFuture { ScheduleReplacerClear(context, it, it) }
|
||||
}
|
||||
|
||||
return when (response) {
|
||||
|
||||
@@ -5,4 +5,6 @@ import ru.n08i40k.polytechnic.next.model.Profile
|
||||
|
||||
interface ProfileRepository {
|
||||
suspend fun getProfile(): MyResult<Profile>
|
||||
|
||||
suspend fun setFcmToken(token: String): MyResult<Unit>
|
||||
}
|
||||
@@ -26,4 +26,8 @@ class FakeProfileRepository : ProfileRepository {
|
||||
MyResult.Success(exampleProfile)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setFcmToken(token: String): MyResult<Unit> {
|
||||
return MyResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,18 @@ import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken
|
||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
|
||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||
|
||||
class RemoteProfileRepository(private val context: Context) : ProfileRepository {
|
||||
override suspend fun getProfile(): MyResult<Profile> =
|
||||
withContext(Dispatchers.IO) {
|
||||
tryFuture {
|
||||
UsersMeRequest(
|
||||
context,
|
||||
it,
|
||||
it
|
||||
)
|
||||
}
|
||||
tryFuture { ProfileMe(context, it, it) }
|
||||
}
|
||||
|
||||
override suspend fun setFcmToken(token: String): MyResult<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
tryFuture { FcmSetToken(context, token, it, it) }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package ru.n08i40k.polytechnic.next.network
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package ru.n08i40k.polytechnic.next.network
|
||||
|
||||
object NetworkValues {
|
||||
const val API_HOST = "https://polytechnic.n08i40k.ru:5050/api/v1/"
|
||||
const val API_HOST = "https://192.168.0.103:5050/api/v1/"
|
||||
}
|
||||
@@ -10,7 +10,9 @@ fun <ResultT, RequestT : RequestBase> tryFuture(
|
||||
buildRequest: (RequestFuture<ResultT>) -> RequestT
|
||||
): MyResult<ResultT> {
|
||||
val future = RequestFuture.newFuture<ResultT>()
|
||||
|
||||
buildRequest(future).send()
|
||||
|
||||
return tryGet(future)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangePasswordRequestData(val oldPassword: String, val newPassword: String)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginRequestData(val username: String, val password: String)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginResponseData(val id: String, val accessToken: String)
|
||||
@@ -1,12 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequestData(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val group: String,
|
||||
val role: UserRole
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RegisterResponseData(val id: String, val accessToken: String)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangeGroupRequestData(val group: String)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangeUsernameRequestData(val username: String)
|
||||
@@ -1,18 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ScheduleGetCacheStatusReq(
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetCacheStatusResData>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.GET,
|
||||
"schedule/cache-status",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetCacheStatusResData(
|
||||
val cacheUpdateRequired: Boolean,
|
||||
val cacheHash: String,
|
||||
val lastCacheUpdate: Long,
|
||||
val lastScheduleUpdate: Long,
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
|
||||
|
||||
class ScheduleGetGroupNamesReq(
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetGroupNamesResData>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : CachedRequest(
|
||||
context,
|
||||
Method.GET,
|
||||
"schedule/get-group-names",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetGroupNamesResData(
|
||||
val names: ArrayList<String>,
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
|
||||
|
||||
class ScheduleGetReq(
|
||||
private val data: ScheduleGetReqData,
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetResData>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : CachedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"schedule/get-group",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetReqData(val name: String)
|
||||
@@ -1,11 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetResData(
|
||||
val updatedAt: String,
|
||||
val group: Group,
|
||||
val lastChangedDays: ArrayList<Int>,
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleUpdateReqData(val mainPage: String)
|
||||
@@ -1,18 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ScheduleReplacerClearReq(
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleReplacerClearResData>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"schedule-replacer/clear",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleReplacerClearResData(
|
||||
val count: Int
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
|
||||
|
||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||
|
||||
typealias ScheduleReplacerGetResData = List<ScheduleReplacer>
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data
|
||||
package ru.n08i40k.polytechnic.next.network.request
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.AuthFailureError
|
||||
@@ -40,7 +40,7 @@ open class AuthorizedRequest(
|
||||
context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
|
||||
}
|
||||
|
||||
if (accessToken.isEmpty())
|
||||
if (accessToken.isEmpty() && context.profileViewModel != null)
|
||||
context.profileViewModel!!.onUnauthorized()
|
||||
|
||||
val headers = super.getHeaders()
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data
|
||||
package ru.n08i40k.polytechnic.next.network.request
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
@@ -11,10 +11,8 @@ import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.network.NetworkConnection
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusReq
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResData
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReq
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReqData
|
||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetCacheStatus
|
||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
|
||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||
import ru.n08i40k.polytechnic.next.network.tryGet
|
||||
import java.util.logging.Logger
|
||||
@@ -58,14 +56,14 @@ open class CachedRequest(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResData> {
|
||||
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatus.ResponseDto> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
when (val mainPage = getMainPage()) {
|
||||
is MyResult.Failure -> mainPage
|
||||
is MyResult.Success -> {
|
||||
tryFuture {
|
||||
ScheduleUpdateReq(
|
||||
ScheduleUpdateReqData(mainPage.data),
|
||||
ScheduleUpdate(
|
||||
ScheduleUpdate.RequestDto(mainPage.data),
|
||||
context,
|
||||
it,
|
||||
it
|
||||
@@ -83,7 +81,7 @@ open class CachedRequest(
|
||||
logger.info("Getting cache status...")
|
||||
|
||||
val cacheStatusResult = tryFuture {
|
||||
ScheduleGetCacheStatusReq(context, it, it)
|
||||
ScheduleGetCacheStatus(context, it, it)
|
||||
}
|
||||
|
||||
if (cacheStatusResult is MyResult.Success) {
|
||||
@@ -1,13 +1,14 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ChangePasswordRequest(
|
||||
private val data: ChangePasswordRequestData,
|
||||
class AuthChangePassword(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
@@ -15,10 +16,13 @@ class ChangePasswordRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"auth/change-password",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
{ listener.onResponse(null) },
|
||||
errorListener,
|
||||
canBeUnauthorized = true
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val oldPassword: String, val newPassword: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class LoginRequest(
|
||||
private val data: LoginRequestData,
|
||||
class AuthLogin(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<LoginResponseData>,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : RequestBase(
|
||||
context,
|
||||
@@ -18,6 +19,12 @@ class LoginRequest(
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val username: String, val password: String)
|
||||
|
||||
@Serializable
|
||||
data class ResponseDto(val id: String, val accessToken: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class RegisterRequest(
|
||||
private val data: RegisterRequestData,
|
||||
class AuthRegister(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<RegisterResponseData>,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : RequestBase(
|
||||
context,
|
||||
@@ -18,6 +20,17 @@ class RegisterRequest(
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val group: String,
|
||||
val role: UserRole
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResponseDto(val id: String, val accessToken: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.n08i40k.polytechnic.next.network.request.fcm
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class FcmSetToken(
|
||||
context: Context,
|
||||
token: String,
|
||||
listener: Response.Listener<Unit>,
|
||||
errorListener: Response.ErrorListener?,
|
||||
) : AuthorizedRequest(
|
||||
context, Method.POST,
|
||||
"fcm/set-token/$token",
|
||||
{ listener.onResponse(Unit) },
|
||||
errorListener,
|
||||
true
|
||||
)
|
||||
@@ -1,13 +1,14 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ChangeGroupRequest(
|
||||
private val data: ChangeGroupRequestData,
|
||||
class ProfileChangeGroup(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
@@ -15,9 +16,12 @@ class ChangeGroupRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"users/change-group",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
{ listener.onResponse(null) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val group: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ChangeUsernameRequest(
|
||||
private val data: ChangeUsernameRequestData,
|
||||
class ProfileChangeUsername(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
@@ -15,9 +16,12 @@ class ChangeUsernameRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"users/change-username",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
{ listener.onResponse(null) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val username: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class UsersMeRequest(
|
||||
class ProfileMe(
|
||||
context: Context,
|
||||
listener: Response.Listener<Profile>,
|
||||
errorListener: Response.ErrorListener?
|
||||
@@ -0,0 +1,36 @@
|
||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||
|
||||
class ScheduleGet(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : CachedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"schedule/get-group",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val name: String)
|
||||
|
||||
@Serializable
|
||||
data class ResponseDto(
|
||||
val updatedAt: String,
|
||||
val group: Group,
|
||||
val lastChangedDays: ArrayList<Int>,
|
||||
)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ScheduleGetCacheStatus(
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.GET,
|
||||
"schedule/cache-status",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class ResponseDto(
|
||||
val cacheUpdateRequired: Boolean,
|
||||
val cacheHash: String,
|
||||
val lastCacheUpdate: Long,
|
||||
val lastScheduleUpdate: Long,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||
|
||||
class ScheduleGetGroupNames(
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : CachedRequest(
|
||||
context,
|
||||
Method.GET,
|
||||
"schedule/get-group-names",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class ResponseDto(
|
||||
val names: ArrayList<String>,
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ScheduleUpdateReq(
|
||||
private val data: ScheduleUpdateReqData,
|
||||
class ScheduleUpdate(
|
||||
private val data: RequestDto,
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetCacheStatusResData>,
|
||||
listener: Response.Listener<ScheduleGetCacheStatus.ResponseDto>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
@@ -18,6 +19,9 @@ class ScheduleUpdateReq(
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class RequestDto(val mainPage: String)
|
||||
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ScheduleReplacerClear(
|
||||
context: Context,
|
||||
listener: Response.Listener<ResponseDto>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"schedule-replacer/clear",
|
||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||
errorListener
|
||||
) {
|
||||
@Serializable
|
||||
data class ResponseDto(
|
||||
val count: Int
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
|
||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||
|
||||
class ScheduleReplacerGetReq(
|
||||
class ScheduleReplacerGet(
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleReplacerGetResData>,
|
||||
listener: Response.Listener<List<ScheduleReplacer>>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
@@ -1,10 +1,10 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
|
||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest
|
||||
|
||||
class ScheduleReplacerSetReq(
|
||||
class ScheduleReplacerSet(
|
||||
context: Context,
|
||||
private val fileName: String,
|
||||
private val fileData: ByteArray,
|
||||
@@ -0,0 +1,110 @@
|
||||
package ru.n08i40k.polytechnic.next.service
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import java.time.Duration
|
||||
|
||||
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<SetFcmTokenWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
|
||||
.setInputData(workDataOf("TOKEN" to token))
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(applicationContext)
|
||||
.enqueue(request)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
val type = message.data["type"]
|
||||
|
||||
when (type) {
|
||||
"schedule-update" -> {
|
||||
val notification = NotificationCompat
|
||||
.Builder(applicationContext, NotificationChannels.SCHEDULE_UPDATE)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setContentTitle(getString(R.string.schedule_update_title))
|
||||
.setContentText(
|
||||
getString(
|
||||
if (message.data["replaced"] == "true")
|
||||
R.string.schedule_update_replaced
|
||||
else
|
||||
R.string.schedule_update_default
|
||||
)
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
with(NotificationManagerCompat.from(this)) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this@MyFirebaseMessagingService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return@with
|
||||
}
|
||||
|
||||
notify(message.data["etag"].hashCode(), notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onMessageReceived(message)
|
||||
}
|
||||
|
||||
class SetFcmTokenWorker(context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
override fun doWork(): Result {
|
||||
val fcmToken = inputData.getString("TOKEN") ?: return Result.failure()
|
||||
|
||||
val accessToken = runBlocking {
|
||||
applicationContext.settingsDataStore.data.map { it.accessToken }.first()
|
||||
}
|
||||
if (accessToken.isEmpty())
|
||||
return Result.retry()
|
||||
|
||||
val setResult = runBlocking {
|
||||
(applicationContext as PolytechnicApplication)
|
||||
.container
|
||||
.profileRepository
|
||||
.setFcmToken(fcmToken)
|
||||
}
|
||||
|
||||
return when (setResult) {
|
||||
is MyResult.Success -> Result.success()
|
||||
is MyResult.Failure -> Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
package ru.n08i40k.polytechnic.next.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
@@ -11,20 +19,95 @@ import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.safeContent
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels.SCHEDULE_UPDATE
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = getString(R.string.schedule_channel_name)
|
||||
val description = getString(R.string.schedule_channel_description)
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel = NotificationChannel(SCHEDULE_UPDATE, name, importance)
|
||||
channel.description = description
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) {
|
||||
if (it) {
|
||||
createNotificationChannel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(): Boolean {
|
||||
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
|
||||
private fun askNotificationPermission() {
|
||||
if (!hasNotificationPermission())
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
class CacheUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
override fun doWork(): Result {
|
||||
runBlocking {
|
||||
(applicationContext as PolytechnicApplication)
|
||||
.container
|
||||
.scheduleRepository
|
||||
.getGroup()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
private fun schedulePeriodicRequest() {
|
||||
val workRequest = PeriodicWorkRequest.Builder(
|
||||
CacheUpdateWorker::class.java,
|
||||
15, TimeUnit.MINUTES
|
||||
)
|
||||
.addTag("schedule-update")
|
||||
.build()
|
||||
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
|
||||
workManager.cancelAllWorkByTag("schedule-update")
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
schedulePeriodicRequest()
|
||||
askNotificationPermission()
|
||||
|
||||
if (hasNotificationPermission())
|
||||
createNotificationChannel()
|
||||
|
||||
setContent {
|
||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||
PolytechnicApp()
|
||||
|
||||
@@ -59,11 +59,9 @@ 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.data.auth.LoginRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
|
||||
import ru.n08i40k.polytechnic.next.network.request.auth.AuthLogin
|
||||
import ru.n08i40k.polytechnic.next.network.request.auth.AuthRegister
|
||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@@ -368,7 +366,7 @@ fun tryLogin(
|
||||
|
||||
isLoading = true
|
||||
|
||||
LoginRequest(LoginRequestData(username, password), context, {
|
||||
AuthLogin(AuthLogin.RequestDto(username, password), context, {
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData { currentSettings ->
|
||||
currentSettings
|
||||
@@ -379,7 +377,7 @@ fun tryLogin(
|
||||
}
|
||||
}
|
||||
|
||||
UsersMeRequest(context, {
|
||||
ProfileMe(context, {
|
||||
scope.launch { snackbarHostState.showSnackbar("Cool!") }
|
||||
|
||||
runBlocking {
|
||||
@@ -437,8 +435,8 @@ fun tryRegister(
|
||||
|
||||
isLoading = true
|
||||
|
||||
RegisterRequest(
|
||||
RegisterRequestData(
|
||||
AuthRegister(
|
||||
AuthRegister.RequestDto(
|
||||
username,
|
||||
password,
|
||||
group,
|
||||
|
||||
@@ -35,9 +35,8 @@ import com.android.volley.ClientError
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesReq
|
||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
|
||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
|
||||
|
||||
private enum class ChangeGroupError {
|
||||
NOT_EXISTS
|
||||
@@ -49,7 +48,7 @@ private fun tryChangeGroup(
|
||||
onError: (ChangeGroupError) -> Unit,
|
||||
onSuccess: (String) -> Unit
|
||||
) {
|
||||
ChangeGroupRequest(ChangeGroupRequestData(group), context, {
|
||||
ProfileChangeGroup(ProfileChangeGroup.RequestDto(group), context, {
|
||||
onSuccess(group)
|
||||
}, {
|
||||
if (it is ClientError && it.networkResponse.statusCode == 404)
|
||||
@@ -65,7 +64,7 @@ private fun getGroups(context: Context): ArrayList<String> {
|
||||
val groups = remember { arrayListOf(groupPlaceholder) }
|
||||
|
||||
LaunchedEffect(groups) {
|
||||
ScheduleGetGroupNamesReq(context, {
|
||||
ScheduleGetGroupNames(context, {
|
||||
groups.clear()
|
||||
groups.addAll(it.names)
|
||||
}, {
|
||||
|
||||
@@ -26,8 +26,7 @@ import com.android.volley.ClientError
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.ChangePasswordRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.ChangePasswordRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.request.auth.AuthChangePassword
|
||||
|
||||
private enum class ChangePasswordError {
|
||||
INCORRECT_CURRENT_PASSWORD,
|
||||
@@ -41,7 +40,7 @@ private fun tryChangePassword(
|
||||
onError: (ChangePasswordError) -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
ChangePasswordRequest(ChangePasswordRequestData(oldPassword, newPassword), context, {
|
||||
AuthChangePassword(AuthChangePassword.RequestDto(oldPassword, newPassword), context, {
|
||||
onSuccess()
|
||||
}, {
|
||||
if (it is ClientError && it.networkResponse.statusCode == 409)
|
||||
|
||||
@@ -24,8 +24,7 @@ import com.android.volley.ClientError
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeUsername
|
||||
|
||||
private enum class ChangeUsernameError {
|
||||
INCORRECT_LENGTH,
|
||||
@@ -38,7 +37,7 @@ private fun tryChangeUsername(
|
||||
onError: (ChangeUsernameError) -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
ChangeUsernameRequest(ChangeUsernameRequestData(username), context, {
|
||||
ProfileChangeUsername(ProfileChangeUsername.RequestDto(username), context, {
|
||||
onSuccess()
|
||||
}, {
|
||||
if (it is ClientError && it.networkResponse.statusCode == 409)
|
||||
|
||||
@@ -41,7 +41,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
|
||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
@@ -17,12 +15,10 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -30,6 +26,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Day
|
||||
@@ -44,26 +44,39 @@ private fun getCurrentMinutes(): Int {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getMinutes(): Int {
|
||||
var value by remember { mutableIntStateOf(getCurrentMinutes()) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
val runnable = {
|
||||
value = getCurrentMinutes()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 60_000)
|
||||
|
||||
onDispose {
|
||||
handler.removeCallbacks(runnable)
|
||||
private fun getMinutes(): Flow<Int> {
|
||||
val value by remember {
|
||||
derivedStateOf {
|
||||
flow {
|
||||
while (true) {
|
||||
emit(getCurrentMinutes())
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
|
||||
val currentMinutes by getMinutes().collectAsStateWithLifecycle(0)
|
||||
|
||||
val filteredLessons = lessons
|
||||
.filterNotNull()
|
||||
.filter {
|
||||
it.time != null
|
||||
&& it.time.start >= currentMinutes
|
||||
&& it.time.end <= currentMinutes
|
||||
}
|
||||
|
||||
if (filteredLessons.isEmpty())
|
||||
return -1
|
||||
|
||||
return lessons.indexOf(filteredLessons[0])
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DayCard(
|
||||
@@ -71,9 +84,26 @@ fun DayCard(
|
||||
day: Day? = FakeScheduleRepository.exampleGroup.days[0],
|
||||
current: Boolean = true
|
||||
) {
|
||||
val defaultCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
val customCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
)
|
||||
val noneCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(containerColor = if (current) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor =
|
||||
if (current) MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
else MaterialTheme.colorScheme.surfaceContainerLowest
|
||||
)
|
||||
) {
|
||||
if (day == null) {
|
||||
Text(
|
||||
@@ -92,87 +122,69 @@ fun DayCard(
|
||||
text = day.name,
|
||||
)
|
||||
|
||||
val currentMinutes = getMinutes()
|
||||
|
||||
val isCurrentLesson: (lesson: Lesson) -> Boolean = {
|
||||
current
|
||||
&& it.time != null
|
||||
&& currentMinutes >= it.time.start
|
||||
&& currentMinutes <= it.time.end
|
||||
}
|
||||
val currentLessonIdx = calculateCurrentLessonIdx(day.lessons)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(0.5.dp)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(0.5.dp)
|
||||
) {
|
||||
if (day.nonNullIndices.isEmpty()) {
|
||||
Text("Can't get schedule!")
|
||||
} else {
|
||||
val defaultCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
val customCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
val noneCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
return@Column
|
||||
}
|
||||
|
||||
)
|
||||
for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) {
|
||||
val lesson = day.lessons[i]!!
|
||||
|
||||
for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) {
|
||||
val lesson = day.lessons[i]!!
|
||||
val cardColors = when (lesson.type) {
|
||||
LessonType.DEFAULT -> defaultCardColors
|
||||
LessonType.CUSTOM -> customCardColors
|
||||
}
|
||||
|
||||
val cardColors = when (lesson.type) {
|
||||
LessonType.DEFAULT -> defaultCardColors
|
||||
LessonType.CUSTOM -> customCardColors
|
||||
}
|
||||
val mutableExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
val mutableExpanded = remember { mutableStateOf(false) }
|
||||
val lessonBoxModifier = remember {
|
||||
Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.clickable { mutableExpanded.value = true }
|
||||
.background(cardColors.containerColor)
|
||||
}
|
||||
|
||||
val lessonBoxModifier = remember {
|
||||
Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.clickable { mutableExpanded.value = true }
|
||||
.background(cardColors.containerColor)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = if (isCurrentLesson(lesson)) lessonBoxModifier.border(
|
||||
border = BorderStroke(
|
||||
3.5.dp,
|
||||
Color(
|
||||
cardColors.containerColor.red * 0.5F,
|
||||
cardColors.containerColor.green * 0.5F,
|
||||
cardColors.containerColor.blue * 0.5F,
|
||||
1F
|
||||
)
|
||||
Box(
|
||||
modifier =
|
||||
if (i == currentLessonIdx) lessonBoxModifier.border(
|
||||
border = BorderStroke(
|
||||
3.5.dp,
|
||||
Color(
|
||||
cardColors.containerColor.red * 0.5F,
|
||||
cardColors.containerColor.green * 0.5F,
|
||||
cardColors.containerColor.blue * 0.5F,
|
||||
1F
|
||||
)
|
||||
) else lessonBoxModifier
|
||||
) {
|
||||
LessonRow(
|
||||
day, lesson, cardColors
|
||||
)
|
||||
}
|
||||
if (i != day.nonNullIndices.last()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.background(noneCardColors.containerColor)
|
||||
) {
|
||||
FreeLessonRow(
|
||||
lesson,
|
||||
day.lessons[day.nonNullIndices[day.nonNullIndices.indexOf(i) + 1]]!!,
|
||||
noneCardColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mutableExpanded.value) LessonExtraInfo(
|
||||
lesson, mutableExpanded
|
||||
)
|
||||
else lessonBoxModifier
|
||||
) {
|
||||
LessonRow(
|
||||
day, lesson, cardColors
|
||||
)
|
||||
}
|
||||
if (i != day.nonNullIndices.last()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.background(noneCardColors.containerColor)
|
||||
) {
|
||||
FreeLessonRow(
|
||||
lesson,
|
||||
day.lessons[day.nonNullIndices[day.nonNullIndices.indexOf(i) + 1]]!!,
|
||||
noneCardColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mutableExpanded.value)
|
||||
LessonExtraInfo(lesson, mutableExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,26 +6,63 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
||||
|
||||
@Composable
|
||||
private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
return remember { lifecycleOwner }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
fun ScheduleScreen(
|
||||
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)),
|
||||
onRefreshSchedule: () -> Unit = {}
|
||||
onRefresh: () -> Unit = {}
|
||||
) {
|
||||
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(uiState) {
|
||||
delay(120_000)
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
val lifecycleOwner = rememberUpdatedLifecycleOwner()
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
onRefresh()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
LoadingContent(
|
||||
empty = when (uiState) {
|
||||
@@ -33,13 +70,14 @@ fun ScheduleScreen(
|
||||
is ScheduleUiState.HasSchedule -> false
|
||||
},
|
||||
loading = uiState.isLoading,
|
||||
onRefresh = { onRefreshSchedule() },
|
||||
onRefresh = onRefresh,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
when (uiState) {
|
||||
is ScheduleUiState.HasSchedule -> {
|
||||
Column {
|
||||
val hasSchedule = uiState as ScheduleUiState.HasSchedule
|
||||
|
||||
UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates)
|
||||
DayPager(hasSchedule.group)
|
||||
}
|
||||
@@ -47,7 +85,7 @@ fun ScheduleScreen(
|
||||
|
||||
is ScheduleUiState.NoSchedule -> {
|
||||
if (!uiState.isLoading) {
|
||||
TextButton(onClick = onRefreshSchedule, modifier = Modifier.fillMaxSize()) {
|
||||
TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) {
|
||||
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,9 @@
|
||||
<string name="bytes">байт</string>
|
||||
<string name="clear_replacers">Удалить всё</string>
|
||||
<string name="set_replacer">Загрузить новое расписание</string>
|
||||
<string name="schedule_channel_name">Обновления расписания</string>
|
||||
<string name="schedule_channel_description">Информирует об обновлении расписания</string>
|
||||
<string name="schedule_update_title">Расписание обновлено.</string>
|
||||
<string name="schedule_update_replaced">Расписание было обновлено Администратором.</string>
|
||||
<string name="schedule_update_default">Расписание было обновлено на сайте политехникума.</string>
|
||||
</resources>
|
||||
@@ -40,4 +40,9 @@
|
||||
<string name="bytes">bytes</string>
|
||||
<string name="clear_replacers">Clear</string>
|
||||
<string name="set_replacer">Set new</string>
|
||||
<string name="schedule_channel_name">Schedule update</string>
|
||||
<string name="schedule_channel_description">Inform when schedule has been updated</string>
|
||||
<string name="schedule_update_title">Schedule has been updated.</string>
|
||||
<string name="schedule_update_replaced">Schedule was updated by Administrator.</string>
|
||||
<string name="schedule_update_default">Schedule was updated on polytechnic website.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user