Администраторам добавлена возможность заменять расписание на текущую неделю.

Исправлены недочёты в меню авторизации и регистрации.

Добавлен класс AuthorizedMultipartRequest для отправки multipart запросов.

Файлы дата классов разбиты на ещё большее количество файлов.

Переименовано большинство классов с сетевыми запросами и их файлов для подгонки под однородный вид.

Убрано много мусора в коде.

Я наконец-то плюс минус разобрался в ViewModel'ах. Очень важная информация.
This commit is contained in:
2024-10-03 01:49:22 +04:00
parent 4db3a7d1c2
commit e694edc334
52 changed files with 981 additions and 238 deletions

2
.gitignore vendored
View File

@@ -13,3 +13,5 @@
.externalNativeBuild
.cxx
local.properties
.kotlin
app/release

View File

@@ -20,9 +20,9 @@
<option name="versions">
<list>
<VersionSetting>
<option name="buildVersion" value="7" />
<option name="displayName" value="1.3.1 (7)" />
<option name="displayVersion" value="1.3.1" />
<option name="buildVersion" value="8" />
<option name="displayName" value="1.3.2 (8)" />
<option name="displayVersion" value="1.3.2" />
</VersionSetting>
</list>
</option>

2
.idea/misc.xml generated
View File

@@ -7,7 +7,7 @@
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
versionCode = 8
versionName = "1.3.2"
versionCode = 9
versionName = "1.4.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -10,8 +10,11 @@ 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.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.data.schedule.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
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.data.users.impl.RemoteProfileRepository
@@ -19,33 +22,42 @@ import javax.inject.Singleton
interface AppContainer {
val applicationContext: Context
val networkCacheRepository: NetworkCacheRepository
val scheduleRepository: ScheduleRepository
val scheduleReplacerRepository: ScheduleReplacerRepository
val profileRepository: ProfileRepository
}
class MockAppContainer(override val applicationContext: Context) : AppContainer {
override val networkCacheRepository: NetworkCacheRepository by lazy { FakeNetworkCacheRepository() }
override val scheduleRepository: ScheduleRepository by lazy { FakeScheduleRepository() }
override val profileRepository: ProfileRepository by lazy { FakeProfileRepository() }
override val networkCacheRepository: NetworkCacheRepository
by lazy { FakeNetworkCacheRepository() }
override val scheduleRepository: ScheduleRepository
by lazy { FakeScheduleRepository() }
override val scheduleReplacerRepository: ScheduleReplacerRepository
by lazy { FakeScheduleReplacerRepository() }
override val profileRepository: ProfileRepository
by lazy { FakeProfileRepository() }
}
class RemoteAppContainer(override val applicationContext: Context) : AppContainer {
override val networkCacheRepository: NetworkCacheRepository by lazy {
LocalNetworkCacheRepository(
applicationContext
)
}
override val scheduleRepository: ScheduleRepository by lazy {
RemoteScheduleRepository(
applicationContext
)
}
override val profileRepository: ProfileRepository by lazy {
RemoteProfileRepository(
applicationContext
)
}
override val networkCacheRepository: NetworkCacheRepository
by lazy { LocalNetworkCacheRepository(applicationContext) }
override val scheduleRepository: ScheduleRepository
by lazy { RemoteScheduleRepository(applicationContext) }
override val scheduleReplacerRepository: ScheduleReplacerRepository
by lazy { RemoteScheduleReplacerRepository(applicationContext) }
override val profileRepository: ProfileRepository
by lazy { RemoteProfileRepository(applicationContext) }
}
@Module

View File

@@ -4,4 +4,3 @@ sealed interface MyResult<out R> {
data class Success<out T>(val data: T) : MyResult<T>
data class Failure(val exception: Exception) : MyResult<Nothing>
}

View File

@@ -9,8 +9,8 @@ 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.ScheduleGetRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReq
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReqData
import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
@@ -27,8 +27,8 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val response = tryFuture {
ScheduleGetRequest(
ScheduleGetRequestData(groupName),
ScheduleGetReq(
ScheduleGetReqData(groupName),
context,
it,
it

View File

@@ -0,0 +1,16 @@
package ru.n08i40k.polytechnic.next.data.scheduleReplacer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
interface ScheduleReplacerRepository {
suspend fun getAll(): MyResult<List<ScheduleReplacer>>
suspend fun setCurrent(
fileName: String,
fileData: ByteArray,
fileType: String
): MyResult<Unit>
suspend fun clear(): MyResult<Int>
}

View File

@@ -0,0 +1,31 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
class FakeScheduleReplacerRepository : ScheduleReplacerRepository {
companion object {
@Suppress("SpellCheckingInspection")
val exampleReplacers: List<ScheduleReplacer> = listOf(
ScheduleReplacer("test-etag", 236 * 1024),
ScheduleReplacer("frgsjkfhg", 623 * 1024),
)
}
override suspend fun getAll(): MyResult<List<ScheduleReplacer>> {
return MyResult.Success(exampleReplacers)
}
override suspend fun setCurrent(
fileName: String,
fileData: ByteArray,
fileType: String
): MyResult<Unit> {
return MyResult.Success(Unit)
}
override suspend fun clear(): MyResult<Int> {
return MyResult.Success(1)
}
}

View File

@@ -0,0 +1,41 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl
import android.content.Context
import kotlinx.coroutines.Dispatchers
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.tryFuture
class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository {
override suspend fun getAll(): MyResult<List<ScheduleReplacer>> =
withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerGetReq(context, it, it) }
}
override suspend fun setCurrent(
fileName: String,
fileData: ByteArray,
fileType: String
): MyResult<Nothing> =
withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerSetReq(context, fileName, fileData, fileType, it, it) }
}
override suspend fun clear(): MyResult<Int> {
val response = withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerClearReq(context, it, it) }
}
return when (response) {
is MyResult.Failure -> response
is MyResult.Success -> MyResult.Success(response.data.count)
}
}
}

View File

@@ -10,8 +10,8 @@ import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteProfileRepository(private val context: Context) : ProfileRepository {
override suspend fun getProfile(): MyResult<Profile> {
return withContext(Dispatchers.IO) {
override suspend fun getProfile(): MyResult<Profile> =
withContext(Dispatchers.IO) {
tryFuture {
UsersMeRequest(
context,
@@ -20,5 +20,4 @@ class RemoteProfileRepository(private val context: Context) : ProfileRepository
)
}
}
}
}

View File

@@ -0,0 +1,13 @@
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
@Suppress("unused")
@Serializable
class Day(
val name: String,
val nonNullIndices: ArrayList<Int>,
val defaultIndices: ArrayList<Int>,
val customIndices: ArrayList<Int>,
val lessons: ArrayList<Lesson?>
)

View File

@@ -1,42 +1,6 @@
@file:Suppress("unused")
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.utils.EnumAsIntSerializer
@Serializable
data class LessonTime(val start: Int, val end: Int)
private class LessonTypeIntSerializer : EnumAsIntSerializer<LessonType>(
"LessonType",
{ it.value },
{ v -> LessonType.entries.first { it.value == v } }
)
@Serializable(with = LessonTypeIntSerializer::class)
enum class LessonType(val value: Int) {
DEFAULT(0), CUSTOM(1)
}
@Serializable
data class Lesson(
val type: LessonType,
val defaultIndex: Int,
val name: String,
val time: LessonTime?,
val cabinets: ArrayList<String>,
val teacherNames: ArrayList<String>
)
@Serializable
class Day(
val name: String,
val nonNullIndices: ArrayList<Int>,
val defaultIndices: ArrayList<Int>,
val customIndices: ArrayList<Int>,
val lessons: ArrayList<Lesson?>
)
@Serializable
class Group(

View File

@@ -0,0 +1,13 @@
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
@Serializable
data class Lesson(
val type: LessonType,
val defaultIndex: Int,
val name: String,
val time: LessonTime?,
val cabinets: ArrayList<String>,
val teacherNames: ArrayList<String>
)

View File

@@ -0,0 +1,6 @@
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
@Serializable
data class LessonTime(val start: Int, val end: Int)

View File

@@ -0,0 +1,15 @@
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.utils.EnumAsIntSerializer
private class LessonTypeIntSerializer : EnumAsIntSerializer<LessonType>(
"LessonType",
{ it.value },
{ v -> LessonType.entries.first { it.value == v } }
)
@Serializable(with = LessonTypeIntSerializer::class)
enum class LessonType(val value: Int) {
DEFAULT(0), CUSTOM(1)
}

View File

@@ -1,29 +1,6 @@
package ru.n08i40k.polytechnic.next.model
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
private class UserRoleStringSerializer : EnumAsStringSerializer<UserRole>(
"UserRole",
{ it.value },
{ v -> UserRole.entries.first { it.value == v } }
)
@Serializable(with = UserRoleStringSerializer::class)
enum class UserRole(val value: String, val icon: ImageVector, @StringRes val stringId: Int) {
STUDENT("STUDENT", Icons.Filled.Face, R.string.role_student),
TEACHER("TEACHER", Icons.Filled.Person, R.string.role_teacher),
ADMIN("ADMIN", Icons.Filled.Settings, R.string.role_admin)
}
val AcceptableUserRoles = listOf(UserRole.STUDENT, UserRole.TEACHER)
@Serializable
data class Profile(

View File

@@ -0,0 +1,9 @@
package ru.n08i40k.polytechnic.next.model
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleReplacer(
val etag: String,
val size: Int
)

View File

@@ -0,0 +1,29 @@
package ru.n08i40k.polytechnic.next.model
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
private class UserRoleStringSerializer : EnumAsStringSerializer<UserRole>(
"UserRole",
{ it.value },
{ v -> UserRole.entries.first { it.value == v } }
)
@Serializable(with = UserRoleStringSerializer::class)
enum class UserRole(val value: String, val icon: ImageVector, @StringRes val stringId: Int) {
STUDENT("STUDENT", Icons.Filled.Face, R.string.role_student),
TEACHER("TEACHER", Icons.Filled.Person, R.string.role_teacher),
ADMIN("ADMIN", Icons.Filled.Settings, R.string.role_admin);
companion object {
val AcceptableUserRoles = listOf(STUDENT, TEACHER)
}
}

View File

@@ -0,0 +1,130 @@
package ru.n08i40k.polytechnic.next.network
import android.content.Context
import com.android.volley.Response
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.io.IOException
import java.io.UnsupportedEncodingException
import kotlin.math.min
open class AuthorizedMultipartRequest(
context: Context,
method: Int,
url: String,
listener: Response.Listener<String>,
errorListener: Response.ErrorListener?,
canBeUnauthorized: Boolean = false
) : AuthorizedRequest(context, method, url, listener, errorListener, canBeUnauthorized) {
private val twoHyphens = "--"
private val lineEnd = "\r\n"
private val boundary = "apiclient-" + System.currentTimeMillis()
protected open val byteData: Map<String, DataPart>? get() = null
override fun getBodyContentType(): String {
return "multipart/form-data;boundary=$boundary"
}
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["Content-Type"] = bodyContentType
return headers
}
override fun getBody(): ByteArray {
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)
try {
val params = params
if (!params.isNullOrEmpty()) {
textParse(dos, params, paramsEncoding)
}
val data = byteData
if (!data.isNullOrEmpty()) {
dataParse(dos, data)
}
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
return bos.toByteArray()
} catch (e: IOException) {
e.printStackTrace()
}
return ByteArray(0)
}
@Throws(IOException::class)
private fun textParse(
dataOutputStream: DataOutputStream, params: Map<String, String>, encoding: String
) {
try {
for ((key, value) in params) {
buildTextPart(dataOutputStream, key, value)
}
} catch (uee: UnsupportedEncodingException) {
throw RuntimeException("Encoding not supported: $encoding", uee)
}
}
@Throws(IOException::class)
private fun dataParse(dataOutputStream: DataOutputStream, data: Map<String, DataPart>) {
for ((key, value) in data) {
buildDataPart(dataOutputStream, value, key)
}
}
@Throws(IOException::class)
private fun buildTextPart(
dataOutputStream: DataOutputStream, parameterName: String, parameterValue: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd")
dataOutputStream.writeBytes(lineEnd)
dataOutputStream.writeBytes(parameterValue + lineEnd)
}
@Throws(IOException::class)
private fun buildDataPart(
dataOutputStream: DataOutputStream, dataFile: DataPart, inputName: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes(
"Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd
)
if (dataFile.type != null && dataFile.type!!.trim { it <= ' ' }
.isNotEmpty()) dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd)
dataOutputStream.writeBytes(lineEnd)
val fileInputStream = ByteArrayInputStream(dataFile.content)
var bytesAvailable = fileInputStream.available()
val maxBufferSize = 1024 * 1024
var bufferSize = min(bytesAvailable.toDouble(), maxBufferSize.toDouble()).toInt()
val buffer = ByteArray(bufferSize)
var bytesRead = fileInputStream.read(buffer, 0, bufferSize)
while (bytesRead > 0) {
dataOutputStream.write(buffer, 0, bufferSize)
bytesAvailable = fileInputStream.available()
bufferSize = min(bytesAvailable.toDouble(), maxBufferSize.toDouble()).toInt()
bytesRead = fileInputStream.read(buffer, 0, bufferSize)
}
dataOutputStream.writeBytes(lineEnd)
}
inner class DataPart(name: String?, data: ByteArray, mimeType: String? = null) {
var fileName: String? = name
var content: ByteArray = data
var type: String? = mimeType
}
}

View File

@@ -4,23 +4,14 @@ import android.annotation.SuppressLint
import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.HurlStack
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import ru.n08i40k.polytechnic.next.data.MyResult
import java.security.cert.X509Certificate
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
import java.util.logging.Logger
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class NetworkConnection(ctx: Context) {
companion object {
@Volatile
@@ -66,44 +57,3 @@ class NetworkConnection(ctx: Context) {
requestQueue.add(req)
}
}
open class RequestBase(
protected val context: Context,
method: Int,
url: String?,
listener: Response.Listener<String>,
errorListener: Response.ErrorListener?
) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) {
open fun send() {
Logger.getLogger("RequestBase").info("Sending request to $url")
NetworkConnection.getInstance(context).addToRequestQueue(this)
}
override fun getHeaders(): MutableMap<String, String> {
val headers = mutableMapOf<String, String>()
headers["Content-Type"] = "application/json; charset=utf-8"
headers["version"] = "1"
return headers
}
}
fun <ResultT, RequestT : RequestBase> tryFuture(
buildRequest: (RequestFuture<ResultT>) -> RequestT
): MyResult<ResultT> {
val future = RequestFuture.newFuture<ResultT>()
buildRequest(future).send()
return tryGet(future)
}
fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
return try {
MyResult.Success(future.get())
} catch (exception: VolleyError) {
MyResult.Failure(exception)
} catch (exception: ExecutionException) {
MyResult.Failure(exception.cause as VolleyError)
} catch (exception: TimeoutException) {
MyResult.Failure(exception)
}
}

View File

@@ -0,0 +1,27 @@
package ru.n08i40k.polytechnic.next.network
import android.content.Context
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import java.util.logging.Logger
open class RequestBase(
protected val context: Context,
method: Int,
url: String?,
listener: Response.Listener<String>,
errorListener: Response.ErrorListener?
) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) {
open fun send() {
Logger.getLogger("RequestBase").info("Sending request to $url")
NetworkConnection.getInstance(context).addToRequestQueue(this)
}
override fun getHeaders(): MutableMap<String, String> {
val headers = mutableMapOf<String, String>()
headers["Content-Type"] = "application/json; charset=utf-8"
headers["version"] = "1"
return headers
}
}

View File

@@ -0,0 +1,27 @@
package ru.n08i40k.polytechnic.next.network
import com.android.volley.VolleyError
import com.android.volley.toolbox.RequestFuture
import ru.n08i40k.polytechnic.next.data.MyResult
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
fun <ResultT, RequestT : RequestBase> tryFuture(
buildRequest: (RequestFuture<ResultT>) -> RequestT
): MyResult<ResultT> {
val future = RequestFuture.newFuture<ResultT>()
buildRequest(future).send()
return tryGet(future)
}
fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
return try {
MyResult.Success(future.get())
} catch (exception: VolleyError) {
MyResult.Failure(exception)
} catch (exception: ExecutionException) {
MyResult.Failure(exception.cause as VolleyError)
} catch (exception: TimeoutException) {
MyResult.Failure(exception)
}
}

View File

@@ -11,10 +11,10 @@ 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.ScheduleGetCacheStatusRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData
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.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet
import java.util.logging.Logger
@@ -58,14 +58,14 @@ open class CachedRequest(
}
}
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResponse> {
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResData> {
return withContext(Dispatchers.IO) {
when (val mainPage = getMainPage()) {
is MyResult.Failure -> mainPage
is MyResult.Success -> {
tryFuture {
ScheduleUpdateRequest(
ScheduleUpdateRequestData(mainPage.data),
ScheduleUpdateReq(
ScheduleUpdateReqData(mainPage.data),
context,
it,
it
@@ -83,7 +83,7 @@ open class CachedRequest(
logger.info("Getting cache status...")
val cacheStatusResult = tryFuture {
ScheduleGetCacheStatusRequest(context, it, it)
ScheduleGetCacheStatusReq(context, it, it)
}
if (cacheStatusResult is MyResult.Success) {

View File

@@ -15,7 +15,7 @@ class LoginRequest(
context,
Method.POST,
"auth/sign-in",
Response.Listener<String> { response -> listener.onResponse(Json.decodeFromString(response)) },
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
override fun getBody(): ByteArray {

View File

@@ -15,7 +15,7 @@ class RegisterRequest(
context,
Method.POST,
"auth/sign-up",
Response.Listener<String> { response -> listener.onResponse(Json.decodeFromString(response)) },
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
override fun getBody(): ByteArray {

View File

@@ -11,9 +11,9 @@ class UsersMeRequest(
listener: Response.Listener<Profile>,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
context, Method.GET, "users/me", Response.Listener<String> { response ->
listener.onResponse(
Json.decodeFromString<Profile>(response)
)
}, errorListener
context,
Method.GET,
"users/me",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -5,12 +5,14 @@ import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleGetCacheStatusRequest(
class ScheduleGetCacheStatusReq(
context: Context,
listener: Response.Listener<ScheduleGetCacheStatusResponse>,
listener: Response.Listener<ScheduleGetCacheStatusResData>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context, Method.GET, "schedule/cache-status", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetCacheStatusResponse>(response))
}, errorListener
context,
Method.GET,
"schedule/cache-status",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -3,7 +3,7 @@ package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetCacheStatusResponse(
data class ScheduleGetCacheStatusResData(
val cacheUpdateRequired: Boolean,
val cacheHash: String,
val lastCacheUpdate: Long,

View File

@@ -5,12 +5,14 @@ import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetGroupNamesRequest(
class ScheduleGetGroupNamesReq(
context: Context,
listener: Response.Listener<ScheduleGetGroupNamesResponseData>,
listener: Response.Listener<ScheduleGetGroupNamesResData>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context, Method.GET, "schedule/get-group-names", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetGroupNamesResponseData>(response))
}, errorListener
context,
Method.GET,
"schedule/get-group-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -3,6 +3,6 @@ package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetGroupNamesResponseData(
data class ScheduleGetGroupNamesResData(
val names: ArrayList<String>,
)

View File

@@ -6,15 +6,17 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetRequest(
private val data: ScheduleGetRequestData,
class ScheduleGetReq(
private val data: ScheduleGetReqData,
context: Context,
listener: Response.Listener<ScheduleGetResponse>,
listener: Response.Listener<ScheduleGetResData>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context, Method.POST, "schedule/get-group", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetResponse>(response))
}, errorListener
context,
Method.POST,
"schedule/get-group",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray()

View File

@@ -3,4 +3,4 @@ package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetRequestData(val name: String)
data class ScheduleGetReqData(val name: String)

View File

@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.model.Group
@Serializable
data class ScheduleGetResponse(
data class ScheduleGetResData(
val updatedAt: String,
val group: Group,
val lastChangedDays: ArrayList<Int>,

View File

@@ -6,15 +6,17 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleUpdateRequest(
private val data: ScheduleUpdateRequestData,
class ScheduleUpdateReq(
private val data: ScheduleUpdateReqData,
context: Context,
listener: Response.Listener<ScheduleGetCacheStatusResponse>,
listener: Response.Listener<ScheduleGetCacheStatusResData>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context, Method.POST, "schedule/update-site-main-page", Response.Listener<String> {
listener.onResponse(Json.decodeFromString<ScheduleGetCacheStatusResponse>(it))
}, errorListener
context,
Method.POST,
"schedule/update-site-main-page",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray()

View File

@@ -3,4 +3,4 @@ package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleUpdateRequestData(val mainPage: String)
data class ScheduleUpdateReqData(val mainPage: String)

View File

@@ -0,0 +1,18 @@
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
)

View File

@@ -0,0 +1,8 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleReplacerClearResData(
val count: Int
)

View File

@@ -0,0 +1,18 @@
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 ScheduleReplacerGetReq(
context: Context,
listener: Response.Listener<ScheduleReplacerGetResData>,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
context,
Method.GET,
"schedule-replacer/get",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -0,0 +1,5 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
typealias ScheduleReplacerGetResData = List<ScheduleReplacer>

View File

@@ -0,0 +1,28 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import android.content.Context
import com.android.volley.Response
import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest
class ScheduleReplacerSetReq(
context: Context,
private val fileName: String,
private val fileData: ByteArray,
private val fileType: String,
private val listener: Response.Listener<Nothing>,
errorListener: Response.ErrorListener?
) : AuthorizedMultipartRequest(
context,
Method.POST,
"schedule-replacer/set",
{ listener.onResponse(null) },
errorListener
) {
override val byteData: Map<String, DataPart>
get() = mapOf(
Pair(
"file",
DataPart(fileName, fileData, fileType)
)
)
}

View File

@@ -23,15 +23,15 @@ fun LoadingContent(
verticalArrangement: Arrangement.Vertical = Arrangement.Center,
content: @Composable () -> Unit
) {
if (empty) emptyContent()
else {
PullToRefreshBox(
isRefreshing = loading, onRefresh = onRefresh
) {
LazyColumn(Modifier.fillMaxSize(), verticalArrangement = verticalArrangement) {
item {
content()
}
if (empty) {
emptyContent()
return
}
PullToRefreshBox(isRefreshing = loading, onRefresh = onRefresh) {
LazyColumn(Modifier.fillMaxSize(), verticalArrangement = verticalArrangement) {
item {
content()
}
}
}
@@ -44,7 +44,5 @@ fun FullScreenLoading() {
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
) {
CircularProgressIndicator()
}
) { CircularProgressIndicator() }
}

View File

@@ -57,8 +57,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.AcceptableUserRoles
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
@@ -125,27 +125,29 @@ private fun LoginForm(
Text(text = stringResource(R.string.not_registered))
}
Button(onClick = {
if (username.length < 4) usernameError = true
if (password.isEmpty()) passwordError = true
Button(
enabled = !mutableIsLoading.value,
onClick = {
if (username.length < 4) usernameError = true
if (password.isEmpty()) passwordError = true
if (usernameError || passwordError) return@Button
if (usernameError || passwordError) return@Button
tryLogin(
username,
password,
mutableUsernameError,
mutablePasswordError,
mutableIsLoading,
context,
snackbarHostState,
scope,
navController
)
tryLogin(
username,
password,
mutableUsernameError,
mutablePasswordError,
mutableIsLoading,
context,
snackbarHostState,
scope,
navController
)
mutableIsLoading.value = true
focusManager.clearFocus()
}) {
focusManager.clearFocus()
}
) {
Text(
text = stringResource(R.string.login),
style = MaterialTheme.typography.bodyLarge
@@ -253,9 +255,9 @@ private fun RegisterForm(
navController
)
mutableIsLoading.value = true
focusManager.clearFocus()
}) {
}
) {
Text(
text = stringResource(R.string.register),
style = MaterialTheme.typography.bodyLarge
@@ -364,9 +366,9 @@ fun tryLogin(
) {
var isLoading by mutableIsLoading
LoginRequest(LoginRequestData(username, password), context, {
scope.launch { snackbarHostState.showSnackbar("Cool!") }
isLoading = true
LoginRequest(LoginRequestData(username, password), context, {
runBlocking {
context.settingsDataStore.updateData { currentSettings ->
currentSettings
@@ -378,6 +380,8 @@ fun tryLogin(
}
UsersMeRequest(context, {
scope.launch { snackbarHostState.showSnackbar("Cool!") }
runBlocking {
context.settingsDataStore.updateData { currentSettings ->
currentSettings
@@ -388,7 +392,7 @@ fun tryLogin(
}
navController.navigate("main")
}, {}).send()
}, null).send()
}, {
isLoading = false
@@ -431,6 +435,8 @@ fun tryRegister(
) {
var isLoading by mutableIsLoading
isLoading = true
RegisterRequest(
RegisterRequestData(
username,

View File

@@ -3,17 +3,22 @@ package ru.n08i40k.polytechnic.next.ui.main
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.ui.graphics.vector.ImageVector
import ru.n08i40k.polytechnic.next.R
data class BottomNavItem(
@StringRes val label: Int, val icon: ImageVector, val route: String
@StringRes val label: Int,
val icon: ImageVector,
val route: String,
val isAdmin: Boolean = false
)
object Constants {
val bottomNavItem = listOf(
BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"),
BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", true),
BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule")
)
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -31,10 +32,14 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
@@ -43,13 +48,14 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
private fun NavHostContainer(
navController: NavHostController,
padding: PaddingValues,
scheduleViewModel: ScheduleViewModel
scheduleViewModel: ScheduleViewModel,
scheduleReplacerViewModel: ScheduleReplacerViewModel?
) {
val context = LocalContext.current
NavHost(
navController = navController,
startDestination = Constants.bottomNavItem[1].route,
startDestination = "schedule",
modifier = Modifier.padding(paddingValues = padding),
enterTransition = {
slideIn(
@@ -76,27 +82,36 @@ private fun NavHostContainer(
composable("schedule") {
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
}
if (scheduleReplacerViewModel != null) {
composable("replacer") {
ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
}
}
})
}
@Composable
private fun BottomNavBar(navController: NavHostController) {
private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Constants.bottomNavItem.forEach { navItem ->
Constants.bottomNavItem.forEach {
if (it.isAdmin && !isAdmin)
return@forEach
NavigationBarItem(
selected = navItem.route == currentRoute,
onClick = { if (navItem.route != currentRoute) navController.navigate(navItem.route) },
selected = it.route == currentRoute,
onClick = { if (it.route != currentRoute) navController.navigate(it.route) },
icon = {
Icon(
imageVector = navItem.icon,
contentDescription = stringResource(navItem.label)
imageVector = it.icon,
contentDescription = stringResource(it.label)
)
},
label = { Text(stringResource(navItem.label)) })
label = { Text(stringResource(it.label)) })
}
}
}
@@ -126,14 +141,23 @@ fun MainScreen(
onUnauthorized = { appNavController.navigate("auth") })
)
val profileUiState by LocalContext.current.profileViewModel!!.uiState.collectAsStateWithLifecycle()
val isAdmin = (profileUiState is ProfileUiState.HasProfile) &&
(profileUiState as ProfileUiState.HasProfile).profile.role == UserRole.ADMIN
val scheduleReplacerViewModel: ScheduleReplacerViewModel? =
if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity)
else null
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomNavBar(navController = navController) }
bottomBar = { BottomNavBar(navController, isAdmin) }
) { paddingValues ->
NavHostContainer(
navController,
paddingValues,
scheduleViewModel
scheduleViewModel,
scheduleReplacerViewModel
)
}
}

View File

@@ -37,7 +37,7 @@ 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.ScheduleGetGroupNamesRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesReq
private enum class ChangeGroupError {
NOT_EXISTS
@@ -65,7 +65,7 @@ private fun getGroups(context: Context): ArrayList<String> {
val groups = remember { arrayListOf(groupPlaceholder) }
LaunchedEffect(groups) {
ScheduleGetGroupNamesRequest(context, {
ScheduleGetGroupNamesReq(context, {
groups.clear()
groups.addAll(it.names)
}, {

View File

@@ -0,0 +1,245 @@
package ru.n08i40k.polytechnic.next.ui.main.replacer
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
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.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.ui.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun ReplacerScreen(
scheduleReplacerViewModel: ScheduleReplacerViewModel = ScheduleReplacerViewModel(
MockAppContainer(
LocalContext.current
)
),
refresh: () -> Unit = {}
) {
val uiState by scheduleReplacerViewModel.uiState.collectAsStateWithLifecycle()
var uri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
uri = it
}
UploadFile(scheduleReplacerViewModel, uri) { uri = null }
LoadingContent(
empty = when (uiState) {
is ScheduleReplacerUiState.NoData -> uiState.isLoading
is ScheduleReplacerUiState.HasData -> false
},
loading = uiState.isLoading,
onRefresh = refresh,
verticalArrangement = Arrangement.Top,
content = {
when (uiState) {
is ScheduleReplacerUiState.NoData -> {
if (!uiState.isLoading) {
TextButton(onClick = refresh, modifier = Modifier.fillMaxSize()) {
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
}
}
}
is ScheduleReplacerUiState.HasData -> {
Column {
Row(modifier = Modifier.fillMaxWidth()) {
ClearButton(Modifier.fillMaxWidth(0.5F)) {
scheduleReplacerViewModel.clear()
}
SetNewButton(Modifier.fillMaxWidth()) {
launcher.launch(arrayOf("application/vnd.ms-excel"))
}
}
ReplacerList((uiState as ScheduleReplacerUiState.HasData).replacers)
}
}
}
}
)
}
@Composable
fun UploadFile(
scheduleReplacerViewModel: ScheduleReplacerViewModel,
uri: Uri?,
onFinish: () -> Unit
) {
if (uri == null)
return
val context = LocalContext.current
val contentResolver = context.contentResolver
// get file name
val query = contentResolver.query(uri, null, null, null, null)
if (query == null) {
onFinish()
return
}
val fileName = query.use { cursor ->
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIdx)
}
// get file type
val fileType: String? = contentResolver.getType(uri)
if (fileType == null) {
onFinish()
return
}
// get file data
val inputStream = contentResolver.openInputStream(uri)
if (inputStream == null) {
onFinish()
return
}
val fileData = inputStream.readBytes()
inputStream.close()
scheduleReplacerViewModel.set(fileName, fileData, fileType)
onFinish()
}
//@Preview(showBackground = true)
//@Composable
//private fun UploadFileDialog(
// opened: Boolean = true,
// onClose: () -> Unit = {}
//) {
// Dialog(onDismissRequest = onClose) {
// Card {
// Button
// }
// }
//}
@Preview(showBackground = true)
@Composable
private fun SetNewButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
Button(modifier = modifier, onClick = onClick) {
val setReplacerText = stringResource(R.string.set_replacer)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(imageVector = Icons.Filled.Add, contentDescription = setReplacerText)
Text(text = setReplacerText)
Icon(imageVector = Icons.Filled.Add, contentDescription = setReplacerText)
}
}
}
@Preview(showBackground = true)
@Composable
private fun ClearButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
Button(modifier = modifier, onClick = onClick) {
val clearReplacersText = stringResource(R.string.clear_replacers)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(imageVector = Icons.Filled.Delete, contentDescription = clearReplacersText)
Text(text = clearReplacersText)
Icon(imageVector = Icons.Filled.Delete, contentDescription = clearReplacersText)
}
}
}
@Preview(showBackground = true)
@Composable
private fun ReplacerElement(replacer: ScheduleReplacer = FakeScheduleReplacerRepository.exampleReplacers[0]) {
Column(
modifier = Modifier.border(
BorderStroke(
Dp.Hairline,
MaterialTheme.colorScheme.inverseSurface
)
)
) {
val modifier = Modifier.fillMaxWidth()
Text(modifier = modifier, textAlign = TextAlign.Center, text = replacer.etag)
Text(modifier = modifier, textAlign = TextAlign.Center, text = buildString {
append(replacer.size)
append(" ")
append(stringResource(R.string.bytes))
})
}
}
@Preview(showBackground = true)
@Composable
fun ReplacerList(replacers: List<ScheduleReplacer> = FakeScheduleReplacerRepository.exampleReplacers) {
Surface {
LazyColumn(
contentPadding = PaddingValues(0.dp, 5.dp),
modifier = Modifier
.fillMaxWidth()
.height(500.dp)
) {
items(replacers) {
ReplacerElement(it)
}
}
}
}

View File

@@ -16,7 +16,7 @@ import ru.n08i40k.polytechnic.next.model.Group
import java.util.Calendar
import kotlin.math.absoluteValue
@Preview(showBackground = true, showSystemUi = true)
@Preview
@Composable
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)

View File

@@ -0,0 +1,115 @@
package ru.n08i40k.polytechnic.next.ui.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import javax.inject.Inject
sealed interface ScheduleReplacerUiState {
val isLoading: Boolean
data class NoData(
override val isLoading: Boolean,
) : ScheduleReplacerUiState
data class HasData(
override val isLoading: Boolean,
val replacers: List<ScheduleReplacer>,
) : ScheduleReplacerUiState
}
private data class ScheduleReplacerViewModelState(
val isLoading: Boolean = false,
val replacers: List<ScheduleReplacer>? = null,
) {
fun toUiState(): ScheduleReplacerUiState =
if (replacers == null)
ScheduleReplacerUiState.NoData(isLoading)
else
ScheduleReplacerUiState.HasData(isLoading, replacers)
}
@HiltViewModel
class ScheduleReplacerViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleReplacerRepository = appContainer.scheduleReplacerRepository
private val viewModelState = MutableStateFlow(ScheduleReplacerViewModelState(isLoading = true))
val uiState = viewModelState
.map(ScheduleReplacerViewModelState::toUiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
init {
refresh()
}
fun refresh() {
setLoading()
viewModelScope.launch { update() }
}
fun set(
fileName: String,
fileData: ByteArray,
fileType: String
) {
setLoading()
viewModelScope.launch {
val result = scheduleReplacerRepository.setCurrent(fileName, fileData, fileType)
if (result is MyResult.Success) update()
else setLoading(false)
}
}
fun clear() {
setLoading()
viewModelScope.launch {
val result = scheduleReplacerRepository.clear()
viewModelState.update {
when (result) {
is MyResult.Failure -> it.copy(isLoading = false)
is MyResult.Success -> it.copy(isLoading = false, replacers = emptyList())
}
}
}
}
private fun setLoading(loading: Boolean = true) {
viewModelState.update { it.copy(isLoading = loading) }
}
private suspend fun update() {
val result = scheduleReplacerRepository.getAll()
viewModelState.update {
when (result) {
is MyResult.Success -> {
it.copy(
replacers = result.data,
isLoading = false
)
}
is MyResult.Failure -> it.copy(
replacers = null,
isLoading = false
)
}
}
}
}

View File

@@ -1,6 +1,6 @@
package ru.n08i40k.polytechnic.next.ui.model
import androidx.lifecycle.ViewModel
кimport androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -14,7 +14,6 @@ import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
import java.util.Date
import java.util.logging.Logger
import javax.inject.Inject
sealed interface ScheduleUiState {
@@ -72,8 +71,6 @@ class ScheduleViewModel @Inject constructor(
is MyResult.Success -> {
val updateDates = networkCacheRepository.getUpdateDates()
Logger.getLogger("ScheduleViewModel").info("Updating...")
it.copy(
group = result.data,
updateDates = updateDates,

View File

@@ -36,4 +36,8 @@
<string name="last_server_cache_update">Последнее обновление кеша</string>
<string name="last_server_schedule_update">Последнее обновление расписания</string>
<string name="update_info_header">Дополнительная информация</string>
<string name="replacer">Заменитель</string>
<string name="bytes">байт</string>
<string name="clear_replacers">Удалить всё</string>
<string name="set_replacer">Загрузить новое расписание</string>
</resources>

View File

@@ -36,4 +36,8 @@
<string name="last_server_cache_update">Last server cache update</string>
<string name="last_server_schedule_update">Last server schedule update</string>
<string name="update_info_header">Additional information</string>
<string name="replacer">Replacer</string>
<string name="bytes">bytes</string>
<string name="clear_replacers">Clear</string>
<string name="set_replacer">Set new</string>
</resources>