Compare commits

...

49 Commits

Author SHA1 Message Date
7de86ebd40 Merge remote-tracking branch 'origin/nikolays' 2025-02-19 18:58:36 +03:00
29fb39e07e start work user card 2025-02-19 18:58:18 +03:00
92ee533955 Visits are not working now 2025-02-19 18:56:27 +03:00
e73263a657 Merge remote-tracking branch 'origin/master' into Nymos-dev 2025-02-19 18:53:03 +03:00
7afe153706 Tweak backend methods 2025-02-19 18:52:20 +03:00
9ff99120a9 Merge remote-tracking branch 'origin/nikolays' 2025-02-19 18:43:50 +03:00
e0e8ee0d58 add swipe refresh to profile and start crating admin panel 2025-02-19 18:42:54 +03:00
8dae85d136 Tweak backend methods 2025-02-19 18:42:42 +03:00
64dddb4495 Implement token cashing
Fix theme
2025-02-19 18:20:44 +03:00
0d24787c17 Fix https 2025-02-19 17:18:35 +03:00
9daaebcd74 Merge remote-tracking branch 'origin/master' into Nymos-dev 2025-02-19 17:11:43 +03:00
86f9ca2c5b Test 2025-02-19 17:11:33 +03:00
0a74b67a07 change manifest 2025-02-19 17:09:51 +03:00
51d708165e Fix logout navigation 2025-02-19 16:47:35 +03:00
6ab14a31a0 Merge remote-tracking branch 'origin/nikolays' 2025-02-19 16:47:02 +03:00
931839da8d Start AdminUseCase 2025-02-19 16:43:23 +03:00
bb7fa56db4 ScanResultScreen finished with mock logic 2025-02-19 16:43:01 +03:00
f9d31d4743 Implement logout on token/credentials corruption 2025-02-19 16:23:18 +03:00
f41e8c734d Fix visitCard date crash 2025-02-19 15:52:33 +03:00
07637a734f Merge remote-tracking branch 'origin/nikolays' 2025-02-19 15:48:18 +03:00
3782f2fc97 Half-done open method
Fix unauthorized profile access
Fix VisitCard date formatting?
2025-02-19 15:47:18 +03:00
a84032c5d3 END REFACTORING OF FONTS!!!!!!!!! 2025-02-19 15:46:29 +03:00
b65a8bb883 Fix DecoratedButton 2025-02-19 15:11:26 +03:00
4110c2118f Merge remote-tracking branch 'origin/nymos-dev' 2025-02-19 15:07:01 +03:00
1beaa7889e Refactor fonts in login screen 2025-02-19 15:04:54 +03:00
ef43127a56 Fix IOException crash 2025-02-19 15:04:44 +03:00
8ed8bdcbd9 Fix VisitCard 2025-02-19 14:26:17 +03:00
9f3e4369fc Merge remote-tracking branch 'origin/master'
# Conflicts:
#	presentation/src/main/java/com/nto/presentation/screens/profileScreen/ProfileScreen.kt
2025-02-19 14:22:36 +03:00
666207c507 Create scan result screen and start refactoring of fonts 2025-02-19 14:21:55 +03:00
04073743f5 Add VisitCardWrapper 2025-02-19 14:18:36 +03:00
8c5e8a5229 Merge branch 'master' of https://gitnto.innovationcampus.ru/Onomatopoeia/onomatopoeia-front 2025-02-19 13:57:57 +03:00
ddbb5fdf8a Half-implement user retrieving logic 2025-02-19 13:57:05 +03:00
a667f9984f Add QR scanning and navigation refactoring 2025-02-19 12:38:18 +03:00
5c3e7bcdee Integrate shared preferences
Half-done user info receiving
2025-02-19 11:57:45 +03:00
507e076eca Add retrofit
Integrate login endpoint
2025-02-19 11:38:13 +03:00
b319718ac6 Migrate email -> login 2025-02-19 11:08:20 +03:00
328daeb684 Implement dummy data layer
Implement dummy domain layer
2025-02-19 10:59:11 +03:00
55b3ee8ee5 Hook navigation up 2025-02-19 10:48:39 +03:00
76f2d243b4 Merge remote-tracking branch 'origin/master' 2025-02-19 10:42:56 +03:00
8f347d2dc8 ProfileScreen UI done 2025-02-19 10:41:30 +03:00
7e6f01351d ProfileScreen UI done
TODO: viewmodel
2025-02-19 10:24:52 +03:00
9257dfa692 Merge remote-tracking branch 'origin/master' 2025-02-18 20:35:06 +03:00
c95c766d51 change statusbar color 2025-02-18 20:34:49 +03:00
3743614b84 Card done 2025-02-18 20:33:24 +03:00
430eddf70f ProfileScreen mid-done 2025-02-18 20:18:03 +03:00
2fdc05b7f6 Merge branch 'master' of https://gitnto.innovationcampus.ru/Onomatopoeia/onomatopoeia-front 2025-02-18 19:46:18 +03:00
8fdff0e02a Prepare profile screen 2025-02-18 19:46:13 +03:00
4a3d3f02b0 Merge pull request 'Добавить Design.md' (#1) from design into master
Reviewed-on: #1
2025-02-18 16:43:59 +00:00
eccc58782a DecoratedButton done 2025-02-18 19:38:28 +03:00
63 changed files with 1685 additions and 56 deletions

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
Onomatopoeia-front

View File

@ -2,7 +2,7 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<SelectionState runConfigName="presentation">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>

5
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@ -9,7 +10,9 @@
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/data" />
<option value="$PROJECT_DIR$/domain" />
<option value="$PROJECT_DIR$/presentation" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />

View File

@ -0,0 +1,57 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

1
.idea/misc.xml generated
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -5,4 +5,5 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.hilt) apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.7.10" apply false
}

View File

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
id("org.jetbrains.kotlin.plugin.serialization")
}
android {
@ -47,4 +48,5 @@ dependencies {
implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
}

View File

@ -0,0 +1,6 @@
package com.nto.data.models
data class LoginResult(
val successful: Boolean,
val message: String?
)

View File

@ -0,0 +1,6 @@
package com.nto.data.models
data class QRDTO(
val id: Long = Long.MAX_VALUE,
val name: String = ""
)

View File

@ -0,0 +1,7 @@
package com.nto.data.models
enum class RequestResult {
OK,
CANCELED,
ERROR
}

View File

@ -0,0 +1,18 @@
package com.nto.data.models
import java.time.LocalDateTime
enum class Position {
DEVELOPER, DESIGNER, TESTER, ANALYST, ADMINISTRATOR
}
data class UserDTO(
val firstName: String = "",
val lastName: String = "",
val patronymic: String = "",
val position: Position = Position.DEVELOPER,
val lastVisit: LocalDateTime = LocalDateTime.now(),
val isError: Boolean = false,
val isUnauthorized: Boolean = false,
val localizedName: String = ""
)

View File

@ -0,0 +1,16 @@
package com.nto.data.models.cards
import com.nto.data.models.QRDTO
import java.time.LocalDateTime
enum class VisitType {
PHONE_ENTRY, CARD_ENTRY
}
data class VisitCardDTO(
val name: String = "",
val id: Long = Long.MAX_VALUE,
val date: LocalDateTime = LocalDateTime.now(),
val visitType: VisitType = VisitType.CARD_ENTRY,
val qrCode: QRDTO = QRDTO()
)

View File

@ -0,0 +1,6 @@
package com.nto.data.models.cards
data class VisitCardWrapper(
val data: List<VisitCardDTO>?,
val isError: Boolean = false,
)

View File

@ -0,0 +1,18 @@
package com.nto.data.repository
import com.nto.data.models.LoginResult
import com.nto.data.models.RequestResult
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardWrapper
interface DataRepository {
suspend fun auth(login: String, password: String): LoginResult
suspend fun saveToken(token: String, login: String)
suspend fun getToken(): String
suspend fun getLogin(): String
suspend fun getInfo(login: String): UserDTO
suspend fun getVisits(id: String): VisitCardWrapper
suspend fun open(): RequestResult
suspend fun logout()
suspend fun invertLock(login: String, status: Boolean): RequestResult
}

View File

@ -0,0 +1,76 @@
package com.nto.data.repository
import android.content.Context
import android.content.Context.MODE_PRIVATE
import com.nto.data.models.LoginResult
import com.nto.data.models.RequestResult
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardWrapper
import com.nto.data.utils.Provider
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Credentials
import javax.inject.Inject
class DataRepositoryImpl @Inject constructor(@ApplicationContext private val context: Context) :
DataRepository {
override suspend fun auth(login: String, password: String): LoginResult {
val token = Credentials.basic(login, password)
val result = Provider.provideRetrofit().auth(
token
).execute()
if (result.isSuccessful) saveToken(token, login)
return LoginResult(result.isSuccessful, result.message())
}
override suspend fun saveToken(token: String, login: String) {
context.getSharedPreferences("auth", MODE_PRIVATE).edit().putString("token", token)
.putString("login", login).apply()
}
override suspend fun getToken(): String {
return context.getSharedPreferences("auth", MODE_PRIVATE).getString("token", "")!!
}
override suspend fun getLogin(): String {
return context.getSharedPreferences("auth", MODE_PRIVATE).getString("login", "")!!
}
override suspend fun getInfo(login: String): UserDTO {
val result = Provider.provideRetrofit().getInfo(token = getToken(), login = login).execute()
return if (result.isSuccessful) result.body()!!
else UserDTO(isError = true, isUnauthorized = result.code() == 403)
}
override suspend fun getVisits(id: String): VisitCardWrapper {
val result = Provider.provideRetrofit().getVisits(getToken(), id.ifBlank { getLogin() }).execute()
return if (result.isSuccessful) {
VisitCardWrapper(result.body()!!)
} else {
VisitCardWrapper(null, true)
}
}
override suspend fun open(): RequestResult {
val result = Provider.provideRetrofit().open(getToken()).execute()
return when (result.code()) {
200 -> RequestResult.OK
400, 403 -> RequestResult.CANCELED
else -> RequestResult.ERROR
}
}
override suspend fun logout() {
context.getSharedPreferences("auth", MODE_PRIVATE).edit().remove("token").remove("login")
.apply()
}
override suspend fun invertLock(login: String, status: Boolean): RequestResult {
return when (Provider.provideRetrofit().invertLock(getToken(), login, status).execute()
.code()) {
200 -> RequestResult.OK
400, 403 -> RequestResult.CANCELED
else -> RequestResult.ERROR
}
}
}

View File

@ -1,7 +1,22 @@
package com.nto.data.utils
sealed class Destinations{
import kotlinx.serialization.Serializable
sealed class Destinations {
@Serializable
object Login
@Serializable
object Profile
object Scan
@Serializable
data class Scan(
val value: String
)
@Serializable
object Admin
@Serializable
object Options
}

View File

@ -0,0 +1,22 @@
package com.nto.data.utils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object Provider {
@Provides
@Singleton
fun provideRetrofit(): RetrofitApi {
return Retrofit.Builder()
.baseUrl("https://v-chat.ru/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(RetrofitApi::class.java)
}
}

View File

@ -0,0 +1,31 @@
package com.nto.data.utils
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardDTO
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface RetrofitApi {
@GET("employee/auth")
fun auth(@Header("Authorization") token: String): Call<ResponseBody>
@GET("employee/{login}/info")
fun getInfo(@Header("Authorization") token: String, @Path("login") login: String): Call<UserDTO>
@GET("visits/{login}/visits")
fun getVisits(@Header("Authorization") token: String, @Path("login") login:String): Call<List<VisitCardDTO>>
@GET("visit/open")
fun open(@Header("Authorization") token: String): Call<ResponseBody>
@PUT("employee/invertLock")
fun invertLock(
@Header("Authorization") token: String, @Query("login") login: String, @Body status: Boolean
): Call<ResponseBody>
}

View File

@ -0,0 +1,18 @@
package com.nto.domain.repository
import com.nto.data.models.LoginResult
import com.nto.data.models.RequestResult
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardWrapper
interface DomainRepository {
suspend fun auth(email: String, password: String): LoginResult
suspend fun saveToken(token: String, login: String)
suspend fun getToken(): String?
suspend fun getLogin(): String
suspend fun getInfo(login: String): UserDTO
suspend fun getVisits(id: String): VisitCardWrapper
suspend fun open(): RequestResult
suspend fun logout()
suspend fun invertLock(id: String, status: Boolean): RequestResult
}

View File

@ -0,0 +1,67 @@
package com.nto.domain.repository
import com.nto.data.models.LoginResult
import com.nto.data.models.RequestResult
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardWrapper
import com.nto.data.repository.DataRepositoryImpl
import java.io.IOException
import javax.inject.Inject
class DomainRepositoryImpl @Inject constructor(private val dataRepositoryImpl: DataRepositoryImpl) :
DomainRepository {
override suspend fun auth(email: String, password: String): LoginResult {
return try {
dataRepositoryImpl.auth(
login = email, password = password
)
} catch (e: IOException) {
LoginResult(false, "IO exception was thrown: ${e.message}")
}
}
override suspend fun saveToken(token: String, login: String) {
dataRepositoryImpl.saveToken(token, login)
}
override suspend fun getToken(): String? {
return try {
dataRepositoryImpl.getToken()
} catch (e: IOException) {
null
}
}
override suspend fun getLogin(): String {
return dataRepositoryImpl.getLogin()
}
override suspend fun getInfo(login: String): UserDTO {
return try {
return dataRepositoryImpl.getInfo(login.ifBlank { getLogin() })
} catch (e: IOException) {
UserDTO(isError = true, isUnauthorized = true)
}
}
override suspend fun getVisits(id: String): VisitCardWrapper {
return try {
dataRepositoryImpl.getVisits(id)
} catch (e: IOException) {
VisitCardWrapper(null, isError = true)
}
}
override suspend fun open(): RequestResult {
return dataRepositoryImpl.open()
}
override suspend fun logout() {
dataRepositoryImpl.logout()
}
override suspend fun invertLock(id: String, status: Boolean): RequestResult {
return dataRepositoryImpl.invertLock(id, status)
}
}

View File

@ -0,0 +1,10 @@
package com.nto.domain.usecase
import com.nto.domain.repository.DomainRepositoryImpl
import javax.inject.Inject
class AdminUseCase @Inject constructor(private val domainRepositoryImpl: DomainRepositoryImpl) {
suspend fun invertLock(id: String, status: Boolean){
domainRepositoryImpl.invertLock(id, status)
}
}

View File

@ -0,0 +1,15 @@
package com.nto.domain.usecase
import com.nto.data.models.LoginResult
import com.nto.domain.repository.DomainRepositoryImpl
import javax.inject.Inject
class LoginUseCase @Inject constructor(private val domainRepositoryImpl: DomainRepositoryImpl) {
fun checkCredentials(email: String, password: String): Boolean {
return password.isNotBlank() && email.isNotBlank()
}
suspend fun auth(email: String, password: String): LoginResult {
return domainRepositoryImpl.auth(email, password)
}
}

View File

@ -0,0 +1,18 @@
package com.nto.domain.usecase
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardWrapper
import com.nto.domain.repository.DomainRepositoryImpl
import javax.inject.Inject
class ProfileUseCase @Inject constructor(private val domainRepositoryImpl: DomainRepositoryImpl) {
suspend fun getInfo(): UserDTO{
return domainRepositoryImpl.getInfo("")
}
suspend fun logout(){
return domainRepositoryImpl.logout()
}
suspend fun getVisits(): VisitCardWrapper {
return domainRepositoryImpl.getVisits("")
}
}

View File

@ -0,0 +1,4 @@
package com.nto.domain.usecase
class ScanUseCase {
}

View File

@ -0,0 +1,10 @@
package com.nto.domain.usecase
import com.nto.domain.repository.DomainRepositoryImpl
import javax.inject.Inject
class SplashScreenUseCase @Inject constructor(private val domainRepositoryImpl: DomainRepositoryImpl) {
suspend fun getToken(): String? {
return domainRepositoryImpl.getToken()
}
}

View File

@ -7,7 +7,7 @@ junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2024.04.01"
composeBom = "2024.09.03"
kapt = "2.1.10"

View File

@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
id("org.jetbrains.kotlin.plugin.serialization")
}
android {
@ -69,8 +70,9 @@ dependencies {
kapt(libs.hilt.compiler)
implementation(libs.hilt)
implementation (libs.hilt.navigation)
implementation(libs.navigation)
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
implementation("androidx.navigation:navigation-compose:2.8.3")
lintChecks(libs.lint.checks)
lintChecks(libs.lint.checks.compose)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
}

View File

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".di.App"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".di.App"
android:theme="@style/Theme.Onomatopoeiafront">
<activity
android:name=".MainActivity"
@ -19,6 +20,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".CustomCaptureActivity"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"/>
</application>
</manifest>

View File

@ -0,0 +1,5 @@
package com.nto.presentation
import com.journeyapps.barcodescanner.CaptureActivity
class CustomCaptureActivity : CaptureActivity()

View File

@ -3,34 +3,44 @@ package com.nto.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.navigation.compose.rememberNavController
import com.nto.presentation.composable.Navigation
import com.nto.presentation.screens.splashScreen.SplashScreenViewModel
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.TextColor
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
val viewmodel by viewModels<SplashScreenViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NTOTheme {
//XML SUCKS! We use Jetpack Compose btw :>
this.window.statusBarColor = TextColor.toArgb()
viewmodel.checkLogin().apply {
setContent {
NTOTheme {
//XML SUCKS! We use Jetpack Compose btw :>
val state = viewmodel.state.collectAsState().value
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val navController = rememberNavController()
Navigation(
navController = navController,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
)
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val navController = rememberNavController()
Navigation(
navController = navController,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
skipAuth = state.skipAuth
)
}
}
}
}

View File

@ -0,0 +1,154 @@
package com.nto.presentation.composable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nto.presentation.composable.DecoratedButtonType.Default
import com.nto.presentation.composable.DecoratedButtonType.Disabled
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.raleway
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
enum class DecoratedButtonType {
Default, Disabled
}
object DecoratedButtonValues {
private val DefaultColors: ButtonColors
@Composable get() = ButtonColors(
contentColor = NTOTheme.colors.primaryBackground,
containerColor = NTOTheme.colors.button,
disabledContentColor = Color.Unspecified,
disabledContainerColor = Color.Unspecified
)
private val DisabledColors: ButtonColors
@Composable get() = ButtonColors(
disabledContentColor = NTOTheme.colors.primaryBackground,
disabledContainerColor = NTOTheme.colors.buttonDisabled,
contentColor = NTOTheme.colors.primaryBackground,
containerColor = NTOTheme.colors.button
)
@Composable
fun getDefaultColor(type: DecoratedButtonType): ButtonColors {
return when (type) {
Default -> DefaultColors
Disabled -> DisabledColors
}
}
}
/**
* High level element that utilizes [DecoratedButtonType] to obtain [DecoratedButtonValues] and use it's colors accordingly.
*
* Consists of a button with a text inside it. By fact that's just a wrapper to simplify reusing process.
*
* @param text text to display inside a button.
* @param isDisabled mutable variable that represents button state. Should be in viewmodel or screen state. After being converted to the instance of [DecoratedButtonType] that is used to obtain [ButtonColors] inside [DecoratedButtonValues].
* @param modifier modifier that should contain [Modifier.height] and [Modifier.width] or other size definition to work correctly.
* @param shape element will be clipped to that shape.
* @param onClick function to be invoked on button click.
*
* @sample [DecoratedButtonSample]
*
*/
@Composable
fun DecoratedButton(
text: String,
isDisabled: Boolean,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(10.dp),
onClick: () -> Unit,
) {
NTOTheme {
Button(
modifier = modifier, shape = shape, colors = DecoratedButtonValues.getDefaultColor(
when (isDisabled) {
true -> Disabled
false -> Default
}
), onClick = onClick,
enabled = !isDisabled
) {
Text(
text,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
fontFamily = raleway,
color = NTOTheme.colors.secondaryText
)
}
}
}
@Stable
internal class DecoratedButtonSample {
class SampleViewModel {
private val _state = MutableStateFlow(SampleState())
val state: StateFlow<SampleState>
get() = _state.asStateFlow()
fun setDisabledState(data: Boolean) {
_state.value = _state.value.copy(data = data)
}
}
data class SampleState(
var data: Boolean = false
)
@Preview
@Composable
private fun InputFieldPreview() {
NTOTheme {
val sampleViewModel = SampleViewModel()
val state = sampleViewModel.state.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
//Filled button preset
DecoratedButton(
text = "Get Started",
isDisabled = state.value.data,
modifier = Modifier
.height(60.dp)
.width(300.dp),
shape = RoundedCornerShape(10.dp)
) {
//...
}
//Disabled button preset
Spacer(modifier = Modifier.height(10.dp))
DecoratedButton(
text = "Get Started",
isDisabled = !state.value.data,
modifier = Modifier
.height(60.dp)
.width(300.dp),
shape = RoundedCornerShape(10.dp)
) {
//...
}
}
}
}
}

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
@ -39,6 +40,7 @@ import androidx.compose.ui.unit.sp
import com.nto.presentation.R
import com.nto.presentation.theme.BoxGray
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.raleway
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -56,7 +58,7 @@ import kotlinx.coroutines.flow.asStateFlow
class InputFieldOptions(
val containerColor: Color = BoxGray,
val textFieldColors: TextFieldColors? = null,
val paddingValues: PaddingValues = PaddingValues(start = 20.dp),
val paddingValues: PaddingValues = PaddingValues(start = 10.dp),
val isConfidential: Boolean = false
) {
override fun equals(other: Any?): Boolean {
@ -123,13 +125,18 @@ fun InputField(
focusedIndicatorColor = Color.Transparent,
),
onValueChange = onValueChange,
textStyle = NTOTheme.typography.displaySmall,
textStyle = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = NTOTheme.colors.primaryText,
fontFamily = raleway
),
placeholder = {
Text(
placeholder,
style = NTOTheme.typography.displaySmall,
fontFamily = raleway,
color = NTOTheme.colors.disabledText,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
},
@ -148,7 +155,7 @@ fun InputField(
}
} else null,
visualTransformation = if (!options.isConfidential) VisualTransformation.None
else if (state!!.value) PasswordVisualTransformation()
else if (state!!.value) PasswordVisualTransformation('*')
else VisualTransformation.None)
}
@ -184,7 +191,7 @@ internal class InputFieldSample {
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
placeholder = stringResource(R.string.placholder_email),
placeholder = stringResource(R.string.placholder_login),
onValueChange = sampleViewModel::setText,
options = InputFieldOptions(isConfidential = true)
)

View File

@ -6,22 +6,31 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.nto.data.utils.Destinations
import com.nto.presentation.screens.admin.AdminScreen
import com.nto.presentation.screens.loginScreen.LoginScreen
import com.nto.presentation.screens.profileScreen.ProfileScreen
import com.nto.presentation.screens.scanResult.ScanResultScreen
@Composable
fun Navigation(navController: NavHostController, modifier: Modifier = Modifier) {
fun Navigation(navController: NavHostController, skipAuth: Boolean, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
modifier = modifier,
startDestination = Destinations.Login.toString()
startDestination = if (skipAuth) Destinations.Profile else Destinations.Login
) {
composable(Destinations.Login.toString()) {
composable<Destinations.Login> {
LoginScreen(navController)
}
composable(Destinations.Profile.toString()){
//TODO
composable<Destinations.Profile> {
ProfileScreen(navController)
}
composable(Destinations.Scan.toString()){
composable<Destinations.Scan> {
ScanResultScreen(navController)
}
composable<Destinations.Admin> {
AdminScreen(navController)
}
composable<Destinations.Options> {
//TODO
}
}

View File

@ -0,0 +1,37 @@
package com.nto.presentation.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import com.nto.data.models.UserDTO
import com.nto.presentation.theme.NTOTheme
@Composable
fun UserCard(user: UserDTO, modifier: Modifier = Modifier) {
Column(
Modifier
.fillMaxWidth()
.background(NTOTheme.colors.inputFieldBackground)
) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = "@${user.firstName}")
if (false) {
Text(
text = "разблокировать",
textDecoration = TextDecoration.Underline,
color = NTOTheme.colors.button,
)
} else {
}
}
Text(text = "${user.lastName} ${user.firstName} ${user.patronymic}")
Text(text = user.localizedName)
}
}

View File

@ -0,0 +1,89 @@
package com.nto.presentation.composable.cards
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nto.data.models.cards.VisitCardDTO
import com.nto.data.models.cards.VisitType
import com.nto.presentation.R
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.raleway
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
@Composable
fun VisitCard(data: VisitCardDTO, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.width(365.dp)
.height(70.dp)
.clip(RoundedCornerShape(10.dp))
.background(NTOTheme.colors.inputFieldBackground),
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(start = 15.dp, end = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start) {
Text(
text = data.name,
fontSize = 14.sp,
fontFamily = raleway,
fontWeight = FontWeight.Medium,
color = NTOTheme.colors.primaryText
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = data.id.toString(),
fontSize = 12.sp,
fontFamily = raleway,
color = NTOTheme.colors.primaryText
)
}
Column(horizontalAlignment = Alignment.End) {
val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
Text(
text = data.date.format(dateFormat),
fontSize = 14.sp,
fontFamily = raleway,
color = NTOTheme.colors.disabledText
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = stringResource(if (data.visitType == VisitType.PHONE_ENTRY) R.string.label_qr_login else R.string.label_card_login),
fontFamily = raleway,
fontSize = 12.sp,
color = NTOTheme.colors.disabledText
)
}
}
}
}
@Preview
@Composable
private fun VisitCardPreview() {
NTOTheme {
VisitCard(VisitCardDTO("Кабинет 207"))
}
}

View File

@ -0,0 +1,88 @@
package com.nto.presentation.screens.admin
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.nto.data.utils.Destinations
import com.nto.presentation.R
import com.nto.presentation.composable.InputField
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.playfair
import com.nto.presentation.theme.raleway
@Composable
fun AdminScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: AdminViewModel = hiltViewModel()
) {
Scaffold { innerPaddings ->
Column(
Modifier
.padding(innerPaddings)
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
Spacer(Modifier.height(36.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = stringResource(R.string.admin_header),
fontFamily = playfair,
color = NTOTheme.colors.primaryText,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
IconButton(
onClick = {
navController.popBackStack()
},
modifier = Modifier
.size(44.dp)
.border(BorderStroke(2.dp, NTOTheme.colors.buttonDisabled), CircleShape),
) {
Image(
painter = painterResource(R.drawable.ic_arrow_back),
contentDescription = ""
)
}
}
Spacer(Modifier.height(20.dp))
Text(
stringResource(R.string.text_login),
fontFamily = raleway,
fontWeight = FontWeight.Medium,
color = NTOTheme.colors.primaryText,
modifier = Modifier.padding(start = 10.dp),
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(5.dp))
InputField(
viewModel.login,
placeholder = stringResource(R.string.placholder_login),
onValueChange = viewModel::setLogin
)
}
}
}

View File

@ -0,0 +1,19 @@
package com.nto.presentation.screens.admin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class AdminViewModel @Inject constructor(): ViewModel() {
private var _login by mutableStateOf("")
val login get() = _login
fun setLogin(value: String) {
_login = value
}
}

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -21,6 +22,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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
@ -29,9 +31,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.nto.presentation.R
import com.nto.presentation.composable.DecoratedButton
import com.nto.presentation.composable.InputField
import com.nto.presentation.composable.InputFieldOptions
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.playfair
import com.nto.presentation.theme.raleway
@Composable
fun LoginScreen(
@ -61,16 +66,20 @@ fun LoginScreen(
}
Text(
text = stringResource(R.string.greeting_login),
style = NTOTheme.typography.titleLarge,
fontFamily = playfair,
fontSize = 64.sp,
color = NTOTheme.colors.secondaryText,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.greeting_login_description),
style = NTOTheme.typography.displaySmall,
fontFamily = raleway,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
color = NTOTheme.colors.secondaryText,
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
modifier = Modifier.width(300.dp)
)
Spacer(modifier = Modifier.height(60.dp))
Column(
@ -81,19 +90,21 @@ fun LoginScreen(
RoundedCornerShape(topStart = 21.dp, topEnd = 21.dp)
)
.background(NTOTheme.colors.primaryBackground)
.padding(40.dp)
.padding(start = 24.dp, end = 24.dp, top = 40.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
stringResource(R.string.text_email),
style = NTOTheme.typography.displaySmall,
stringResource(R.string.text_login),
fontFamily = raleway,
fontWeight = FontWeight.Medium,
color = NTOTheme.colors.primaryText,
modifier = Modifier.padding(start = 10.dp),
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(5.dp))
InputField(
state.email,
placeholder = stringResource(R.string.placholder_email),
placeholder = stringResource(R.string.placholder_login),
onValueChange = viewModel::setEmail
)
}
@ -101,7 +112,9 @@ fun LoginScreen(
Column(modifier = Modifier.fillMaxWidth()) {
Text(
stringResource(R.string.text_password),
style = NTOTheme.typography.displaySmall,
fontFamily = raleway,
fontWeight = FontWeight.Medium,
color = NTOTheme.colors.primaryText,
modifier = Modifier.padding(start = 10.dp),
fontSize = 14.sp
)
@ -114,7 +127,15 @@ fun LoginScreen(
)
}
Spacer(Modifier.height(50.dp))
//TODO: LoginButton
DecoratedButton(
stringResource(R.string.action_login),
state.disabled,
modifier = Modifier
.width(364.dp)
.height(60.dp)
) {
viewModel.login(navController)
}
}
}
}

View File

@ -1,18 +1,26 @@
package com.nto.presentation.screens.loginScreen
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.nto.data.utils.Destinations
import com.nto.domain.usecase.LoginUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(): ViewModel(){
class LoginViewModel @Inject constructor(
private val useCase: LoginUseCase, @ApplicationContext private val context: Context
) : ViewModel() {
private val _state = MutableStateFlow(LoginScreenState())
val state: StateFlow<LoginScreenState>
@ -29,12 +37,29 @@ class LoginViewModel @Inject constructor(): ViewModel(){
}
private fun checkInput() {
//TODO: domain level
val state = _state.value
val result = useCase.checkCredentials(state.email, state.password)
_state.tryEmit(_state.value.copy(disabled = !result))
}
fun login(navController: NavHostController) {
viewModelScope.launch(Dispatchers.IO) {
//TODO: domain level
val state = _state.value
val result = useCase.auth(state.email, state.password)
if (result.message != null) {
Dispatchers.Main {
Toast.makeText(context, result.message, Toast.LENGTH_LONG).show()
}
}
if (result.successful) {
Dispatchers.Main {
navController.navigate(Destinations.Profile) {
popUpTo<Destinations.Login> {
inclusive = true
}
}
}
}
}
}
}

View File

@ -0,0 +1,284 @@
package com.nto.presentation.screens.profileScreen
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.nto.data.utils.Destinations
import com.nto.presentation.CustomCaptureActivity
import com.nto.presentation.R
import com.nto.presentation.composable.DecoratedButton
import com.nto.presentation.composable.cards.VisitCard
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.TextGray
import com.nto.presentation.theme.playfair
import com.nto.presentation.theme.raleway
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: ProfileViewModel = hiltViewModel<ProfileViewModel>(),
) {
val state = viewModel.state.collectAsState().value
val scannerLauncher = rememberLauncherForActivityResult(
contract = ScanContract(),
onResult = { result -> navController.navigate(Destinations.Scan(result.contents)) }
)
val scanOptions = ScanOptions()
scanOptions.setPrompt("")
scanOptions.setBeepEnabled(false)
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(false)
scanOptions.setCaptureActivity(CustomCaptureActivity::class.java)
LaunchedEffect(state.isUnauthorized) {
if (state.isUnauthorized) {
navController.navigate(Destinations.Login)
}
}
PullToRefreshBox(
isRefreshing = viewModel.isLoading,
onRefresh = {
viewModel.updateInfo()
}
) {
Column(
modifier = modifier
.background(NTOTheme.colors.primaryBackground)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp)
.verticalScroll(
rememberScrollState()
)
) {
Spacer(modifier = Modifier.height(10.dp))
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.title_profile),
fontFamily = playfair,
color = NTOTheme.colors.primaryText,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(
onClick = {
navController.navigate(Destinations.Admin)
},
modifier = Modifier
.size(44.dp)
.border(
BorderStroke(2.dp, NTOTheme.colors.buttonAdmin),
CircleShape
),
) {
Icon(
painter = painterResource(R.drawable.icon_admin),
contentDescription = null,
tint = NTOTheme.colors.buttonAdmin
)
}
Spacer(modifier = Modifier.width(12.dp))
IconButton(
onClick = {},
modifier = Modifier
.size(44.dp)
.border(
BorderStroke(2.dp, NTOTheme.colors.buttonDisabled),
CircleShape
)
) {
Icon(
painter = painterResource(R.drawable.icon_options),
contentDescription = null,
tint = NTOTheme.colors.buttonDisabled
)
}
Spacer(modifier = Modifier.width(16.dp))
IconButton(
modifier = Modifier.size(44.dp), onClick = {
viewModel.logout(navController)
}, colors = IconButtonDefaults.iconButtonColors(
containerColor = NTOTheme.colors.buttonDisabled
)
) {
Icon(
painter = painterResource(R.drawable.icon_logout),
contentDescription = null,
tint = NTOTheme.colors.primaryBackground
)
}
}
}
Spacer(modifier = Modifier.height(50.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.logo_placeholder_user),
modifier = Modifier
.size(100.dp)
.clip(
CircleShape
),
contentDescription = null
)
Spacer(modifier = Modifier.width(15.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.background(NTOTheme.colors.inputFieldBackground)
.padding(10.dp)
) {
Text(
state.secondName,
fontFamily = raleway,
color = NTOTheme.colors.primaryText,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp
)
Text(
state.firstName,
fontFamily = raleway,
color = NTOTheme.colors.primaryText,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp
)
Text(
state.thirdName,
fontFamily = raleway,
color = NTOTheme.colors.primaryText,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
state.job,
fontWeight = FontWeight.Medium,
fontFamily = raleway,
fontSize = 14.sp,
color = TextGray
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(35.dp)
.clip(RoundedCornerShape(10.dp))
.background(NTOTheme.colors.inputFieldBackground)
.padding(start = 15.dp, end = 15.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
stringResource(R.string.label_last_visit),
fontWeight = FontWeight.Medium,
fontFamily = raleway,
color = NTOTheme.colors.primaryText,
fontSize = 14.sp
)
Text(
state.lastOpen,
fontWeight = FontWeight.Normal,
fontFamily = raleway,
color = NTOTheme.colors.primaryText,
fontSize = 14.sp
)
}
Spacer(modifier = Modifier.height(20.dp))
Text(
stringResource(R.string.label_visits),
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
fontFamily = raleway,
color = NTOTheme.colors.primaryText
)
Spacer(modifier = Modifier.height(10.dp))
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
items(state.visits) { item ->
VisitCard(item)
}
}
}
DecoratedButton(
stringResource(R.string.label_scan),
false,
modifier = Modifier
.padding(bottom = 10.dp)
.fillMaxWidth()
.height(62.dp)
) {
scannerLauncher.launch(scanOptions)
}
}
}
}
}
@Preview
@Composable
private fun ProfileScreenPreview() {
NTOTheme {
ProfileScreen(rememberNavController(), Modifier.fillMaxSize())
}
}

View File

@ -0,0 +1,42 @@
package com.nto.presentation.screens.profileScreen
import android.content.Context
import com.nto.data.models.Position
import com.nto.data.models.UserDTO
import com.nto.data.models.cards.VisitCardDTO
import com.nto.presentation.R
import java.time.format.DateTimeFormatter
data class ProfileState(
var firstName: String = "",
var secondName: String = "",
var thirdName: String = "",
var lastOpen: String = "",
var job: String = "",
var isUnauthorized: Boolean = false,
var visits: List<VisitCardDTO> = listOf()
) {
fun deserialize(o: UserDTO, context: Context) {
val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
this.firstName = o.firstName
this.secondName = o.lastName
this.thirdName = o.patronymic
this.lastOpen = try {
o.lastVisit.format(dateFormat)
} catch (e: NullPointerException) {
context.getString(R.string.label_last_visit_none)
}
this.job = translatePosition(o.position, context)
this.isUnauthorized = o.isUnauthorized
}
private fun translatePosition(position: Position, context: Context): String {
return when (position) {
Position.TESTER -> context.getString(R.string.label_tester)
Position.DEVELOPER -> context.getString(R.string.label_developer)
Position.DESIGNER -> context.getString(R.string.label_designer)
Position.ANALYST -> context.getString(R.string.label_analyst)
Position.ADMINISTRATOR -> context.getString(R.string.label_administrator)
}
}
}

View File

@ -0,0 +1,76 @@
package com.nto.presentation.screens.profileScreen
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import com.nto.data.models.QRDTO
import com.nto.data.models.cards.VisitCardDTO
import com.nto.data.models.cards.VisitType
import com.nto.data.utils.Destinations
import com.nto.domain.usecase.ProfileUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val useCase: ProfileUseCase, @ApplicationContext private val context: Context
) : ViewModel() {
private var _isLoading by mutableStateOf(false)
private val _state = MutableStateFlow(ProfileState())
val isLoading get() = _isLoading
val state: StateFlow<ProfileState>
get() = _state.asStateFlow()
fun updateInfo() {
viewModelScope.launch(Dispatchers.IO) {
_isLoading = true
val result = useCase.getInfo()
val visitsResult = useCase.getVisits()
_state.tryEmit(ProfileState().apply {
deserialize(result, context)
if(!visitsResult.isError) {
visits = visitsResult.data!!
}
})
_isLoading = false
}
}
fun admin(navController: NavController) {
//TODO
}
fun logout(navController: NavController) {
viewModelScope.launch(Dispatchers.IO) {
useCase.logout()
Dispatchers.Main {
navController.navigate(Destinations.Login) {
popUpTo<Destinations.Profile> {
inclusive = true
}
}
}
}
}
fun option(navController: NavController) {
navController.navigate(Destinations.Options)
}
init {
updateInfo()
}
}

View File

@ -0,0 +1,115 @@
package com.nto.presentation.screens.scanResult
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.DefaultShadowColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.nto.data.utils.Destinations
import com.nto.presentation.R
import com.nto.presentation.theme.NTOTheme
import com.nto.presentation.theme.playfair
import com.nto.presentation.theme.raleway
@Composable
fun ScanResultScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: ScanResultViewModel = hiltViewModel()
) {
val state by viewModel.state.collectAsState()
Scaffold(bottomBar = {
val buttonColor = when (state) {
ScanResultState.Success -> NTOTheme.colors.button
ScanResultState.Error -> NTOTheme.colors.buttonAdmin
ScanResultState.Warning -> NTOTheme.colors.warning
}
OutlinedButton(
onClick = {
navController.navigate(Destinations.Profile) {
popUpTo<Destinations.Scan> {
inclusive = true
}
}
},
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = buttonColor,
containerColor = Color.Transparent
),
border = BorderStroke(width = 2.dp, color = buttonColor),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 32.dp)
.fillMaxWidth()
.height(62.dp)
) {
Text(
text = stringResource(R.string.close),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}) { paddingValues ->
Column(
Modifier
.padding(paddingValues)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(
id = when (state) {
ScanResultState.Success -> R.drawable.ic_scan_success
ScanResultState.Error -> R.drawable.ic_scan_error
ScanResultState.Warning -> R.drawable.ic_scan_warning
}
), contentDescription = ""
)
Text(
text = stringResource(R.string.code_scanned),
fontFamily = playfair,
fontSize = 36.sp,
color = NTOTheme.colors.primaryText
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(
when (state) {
ScanResultState.Success -> R.string.code_scanned_success
ScanResultState.Error -> R.string.code_scanned_error
ScanResultState.Warning -> R.string.code_scanned_warning
}
),
fontSize = 20.sp,
fontWeight = FontWeight.Medium,
color = NTOTheme.colors.primaryText,
fontFamily = raleway
)
Spacer(Modifier.height(60.dp))
}
}
}

View File

@ -0,0 +1,5 @@
package com.nto.presentation.screens.scanResult
enum class ScanResultState {
Success, Error, Warning
}

View File

@ -0,0 +1,32 @@
package com.nto.presentation.screens.scanResult
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.navigation.toRoute
import com.nto.data.utils.Destinations
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import kotlin.random.Random
@HiltViewModel
class ScanResultViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
): ViewModel() {
val value = savedStateHandle.toRoute<Destinations.Scan>().value
private val _state = MutableStateFlow(ScanResultState.Success)
val state get() = _state.asStateFlow()
init {
// TODO: create method to scan qr on server
_state.value = when (Random.nextInt(0, 3)) {
0 -> ScanResultState.Success
1 -> ScanResultState.Error
2 -> ScanResultState.Warning
else -> ScanResultState.Warning
}
}
}

View File

@ -0,0 +1,5 @@
package com.nto.presentation.screens.splashScreen
data class SplashScreenState(
val skipAuth: Boolean = false
)

View File

@ -0,0 +1,31 @@
package com.nto.presentation.screens.splashScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nto.domain.usecase.SplashScreenUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SplashScreenViewModel @Inject constructor(private val useCase: SplashScreenUseCase) : ViewModel(){
private val _state = MutableStateFlow(
SplashScreenState()
)
val state: StateFlow<SplashScreenState>
get() = _state.asStateFlow()
fun checkLogin(){
viewModelScope.launch(Dispatchers.IO) {
val token = useCase.getToken()
if (!token.isNullOrBlank()){
_state.tryEmit(_state.value.copy(skipAuth = true))
}
}
}
}

View File

@ -7,8 +7,12 @@ import androidx.compose.runtime.staticCompositionLocalOf
val TextGray = Color(0xFFC4BBC7)
val BoxGray = Color(0xFFF8F0FB)
val Background = Color(0xFFFEFBFF)
val Green = Color(0xFF738D73)
val GreenDisabled = Color(0xFFCAD5CA)
val Error = Color(0xFFD28989)
val Warning = Color(0xFFCFC37F)
val TextColor = Color(0xFF211A1D)
@Immutable
data class AppColors(
@ -20,7 +24,9 @@ data class AppColors(
val secondaryText: Color,
val button: Color,
val buttonDisabled: Color,
val buttonAdmin: Color,
val tint: Color,
val warning: Color,
)
@ -35,18 +41,22 @@ val LocalAppColors = staticCompositionLocalOf {
secondaryText = Color.Unspecified,
button = Color.Unspecified,
buttonDisabled = Color.Unspecified,
tint = Color.Unspecified
buttonAdmin = Color.Unspecified,
tint = Color.Unspecified,
warning = Color.Unspecified
)
}
val extendedColor = AppColors(
primaryBackground = Color.White,
primaryBackground = Background,
secondaryBackground = Color.Black,
inputFieldBackground = BoxGray,
disabledText = TextGray,
primaryText = Color.Black,
primaryText = TextColor,
secondaryText = Color.White,
button = Green,
buttonDisabled = GreenDisabled,
tint = Color.Black
buttonAdmin = Error,
tint = Color.Black,
warning = Warning
)

View File

@ -3,13 +3,34 @@ package com.nto.presentation.theme
import android.annotation.SuppressLint
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.nto.presentation.R
@OptIn(ExperimentalTextApi::class)
val raleway = FontFamily(
Font(R.font.raleway, FontWeight.SemiBold, variationSettings = FontVariation.Settings(
FontVariation.weight(FontWeight.SemiBold.weight)
)),
Font(R.font.raleway, FontWeight.Medium, variationSettings = FontVariation.Settings(
FontVariation.weight(FontWeight.Medium.weight)
)),
Font(R.font.raleway, FontWeight.Normal, variationSettings = FontVariation.Settings(
FontVariation.weight(FontWeight.Normal.weight)
))
)
@OptIn(ExperimentalTextApi::class)
val playfair = FontFamily(
Font(R.font.playfair, FontWeight.Bold, variationSettings = FontVariation.Settings(
FontVariation.weight(FontWeight.Bold.weight)
))
)
private val RalewayFontFamily = FontFamily(
Font(R.font.raleway)
)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7.825,13L13.425,18.6L12,20L4,12L12,4L13.425,5.4L7.825,11H20V13H7.825Z"
android:fillColor="#CAD5CA"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="228dp"
android:height="228dp"
android:viewportWidth="228"
android:viewportHeight="228">
<path
android:pathData="M79.8,161.5L114,127.3L148.2,161.5L161.5,148.2L127.3,114L161.5,79.8L148.2,66.5L114,100.7L79.8,66.5L66.5,79.8L100.7,114L66.5,148.2L79.8,161.5ZM114,209C100.86,209 88.51,206.51 76.95,201.52C65.39,196.53 55.34,189.76 46.79,181.21C38.24,172.66 31.47,162.61 26.48,151.05C21.49,139.49 19,127.14 19,114C19,100.86 21.49,88.51 26.48,76.95C31.47,65.39 38.24,55.34 46.79,46.79C55.34,38.24 65.39,31.47 76.95,26.48C88.51,21.49 100.86,19 114,19C127.14,19 139.49,21.49 151.05,26.48C162.61,31.47 172.66,38.24 181.21,46.79C189.76,55.34 196.53,65.39 201.52,76.95C206.51,88.51 209,100.86 209,114C209,127.14 206.51,139.49 201.52,151.05C196.53,162.61 189.76,172.66 181.21,181.21C172.66,189.76 162.61,196.53 151.05,201.52C139.49,206.51 127.14,209 114,209ZM114,190C135.22,190 153.19,182.64 167.91,167.91C182.64,153.19 190,135.22 190,114C190,92.78 182.64,74.81 167.91,60.09C153.19,45.36 135.22,38 114,38C92.78,38 74.81,45.36 60.09,60.09C45.36,74.81 38,92.78 38,114C38,135.22 45.36,153.19 60.09,167.91C74.81,182.64 92.78,190 114,190Z"
android:fillColor="#D28989"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="228dp"
android:height="228dp"
android:viewportWidth="228"
android:viewportHeight="228">
<path
android:pathData="M63.65,171L9.97,117.32L23.51,104.03L63.89,144.4L77.19,157.7L63.65,171ZM117.32,171L63.65,117.32L76.95,103.79L117.32,144.16L204.73,56.76L218.02,70.3L117.32,171ZM117.32,117.32L103.79,104.03L150.81,57L164.35,70.3L117.32,117.32Z"
android:fillColor="#738D73"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="228dp"
android:height="228dp"
android:viewportWidth="228"
android:viewportHeight="228">
<path
android:pathData="M28.5,190V171H54.63L50.83,167.68C42.59,160.39 36.81,152.08 33.49,142.74C30.16,133.4 28.5,123.97 28.5,114.47C28.5,96.9 33.76,81.26 44.29,67.57C54.82,53.87 68.56,44.81 85.5,40.38V60.33C74.1,64.44 64.92,71.45 57.95,81.34C50.98,91.24 47.5,102.28 47.5,114.47C47.5,121.6 48.85,128.53 51.54,135.26C54.23,141.99 58.42,148.2 64.13,153.9L66.5,156.27V133H85.5V190H28.5ZM114,161.5C111.31,161.5 109.05,160.59 107.23,158.77C105.41,156.95 104.5,154.69 104.5,152C104.5,149.31 105.41,147.05 107.23,145.23C109.05,143.41 111.31,142.5 114,142.5C116.69,142.5 118.95,143.41 120.77,145.23C122.59,147.05 123.5,149.31 123.5,152C123.5,154.69 122.59,156.95 120.77,158.77C118.95,160.59 116.69,161.5 114,161.5ZM104.5,123.5V66.5H123.5V123.5H104.5ZM142.5,187.63V167.68C153.9,163.56 163.08,156.55 170.05,146.66C177.02,136.76 180.5,125.72 180.5,113.53C180.5,106.4 179.15,99.47 176.46,92.74C173.77,86.01 169.57,79.8 163.88,74.1L161.5,71.72V95H142.5V38H199.5V57H173.38L177.18,60.33C184.93,68.08 190.59,76.51 194.16,85.62C197.72,94.72 199.5,104.03 199.5,113.53C199.5,131.1 194.24,146.74 183.71,160.43C173.18,174.13 159.44,183.19 142.5,187.63Z"
android:fillColor="#CFC37F"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M12.279,13.302C11.273,13.302 10.424,12.957 9.734,12.266C9.043,11.576 8.698,10.727 8.698,9.721C8.698,8.715 9.043,7.866 9.734,7.176C10.424,6.485 11.273,6.14 12.279,6.14C13.285,6.14 14.134,6.485 14.824,7.176C15.515,7.866 15.861,8.715 15.861,9.721C15.861,10.727 15.515,11.576 14.824,12.266C14.134,12.957 13.285,13.302 12.279,13.302ZM12.279,11.256C12.722,11.256 13.089,11.111 13.379,10.821C13.669,10.531 13.814,10.164 13.814,9.721C13.814,9.278 13.669,8.911 13.379,8.621C13.089,8.331 12.722,8.186 12.279,8.186C11.836,8.186 11.469,8.331 11.179,8.621C10.889,8.911 10.744,9.278 10.744,9.721C10.744,10.164 10.889,10.531 11.179,10.821C11.469,11.111 11.836,11.256 12.279,11.256ZM12.279,22.512C9.909,21.915 7.952,20.555 6.408,18.431C4.865,16.308 4.093,13.95 4.093,11.358V5.116L12.279,2.047L20.465,5.116V11.358C20.465,13.95 19.693,16.308 18.15,18.431C16.607,20.555 14.65,21.915 12.279,22.512ZM12.279,4.221L6.14,6.523V11.358C6.14,12.279 6.267,13.174 6.523,14.044C6.779,14.914 7.129,15.733 7.572,16.5C8.288,16.142 9.039,15.861 9.823,15.656C10.608,15.451 11.426,15.349 12.279,15.349C13.132,15.349 13.95,15.451 14.735,15.656C15.519,15.861 16.27,16.142 16.986,16.5C17.43,15.733 17.779,14.914 18.035,14.044C18.291,13.174 18.419,12.279 18.419,11.358V6.523L12.279,4.221ZM12.279,17.395C11.665,17.395 11.068,17.464 10.488,17.6C9.909,17.736 9.354,17.924 8.826,18.163C9.32,18.674 9.857,19.118 10.437,19.493C11.017,19.868 11.631,20.158 12.279,20.363C12.927,20.158 13.541,19.868 14.121,19.493C14.701,19.118 15.238,18.674 15.733,18.163C15.204,17.924 14.65,17.736 14.07,17.6C13.49,17.464 12.893,17.395 12.279,17.395Z"
android:fillColor="#D28989"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V5C3,4.45 3.196,3.979 3.588,3.588C3.979,3.196 4.45,3 5,3H12V5H5V19H12V21H5ZM16,17L14.625,15.55L17.175,13H9V11H17.175L14.625,8.45L16,7L21,12L16,17Z"
android:fillColor="#FEFBFF"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M9.25,22L8.85,18.8C8.633,18.717 8.429,18.617 8.238,18.5C8.046,18.383 7.858,18.258 7.675,18.125L4.7,19.375L1.95,14.625L4.525,12.675C4.508,12.558 4.5,12.446 4.5,12.337V11.663C4.5,11.554 4.508,11.442 4.525,11.325L1.95,9.375L4.7,4.625L7.675,5.875C7.858,5.742 8.05,5.617 8.25,5.5C8.45,5.383 8.65,5.283 8.85,5.2L9.25,2H14.75L15.15,5.2C15.367,5.283 15.571,5.383 15.762,5.5C15.954,5.617 16.142,5.742 16.325,5.875L19.3,4.625L22.05,9.375L19.475,11.325C19.492,11.442 19.5,11.554 19.5,11.663V12.337C19.5,12.446 19.483,12.558 19.45,12.675L22.025,14.625L19.275,19.375L16.325,18.125C16.142,18.258 15.95,18.383 15.75,18.5C15.55,18.617 15.35,18.717 15.15,18.8L14.75,22H9.25ZM11,20H12.975L13.325,17.35C13.842,17.217 14.321,17.021 14.762,16.763C15.204,16.504 15.608,16.192 15.975,15.825L18.45,16.85L19.425,15.15L17.275,13.525C17.358,13.292 17.417,13.046 17.45,12.788C17.483,12.529 17.5,12.267 17.5,12C17.5,11.733 17.483,11.471 17.45,11.212C17.417,10.954 17.358,10.708 17.275,10.475L19.425,8.85L18.45,7.15L15.975,8.2C15.608,7.817 15.204,7.496 14.762,7.238C14.321,6.979 13.842,6.783 13.325,6.65L13,4H11.025L10.675,6.65C10.158,6.783 9.679,6.979 9.238,7.238C8.796,7.496 8.392,7.808 8.025,8.175L5.55,7.15L4.575,8.85L6.725,10.45C6.642,10.7 6.583,10.95 6.55,11.2C6.517,11.45 6.5,11.717 6.5,12C6.5,12.267 6.517,12.525 6.55,12.775C6.583,13.025 6.642,13.275 6.725,13.525L4.575,15.15L5.55,16.85L8.025,15.8C8.392,16.183 8.796,16.504 9.238,16.763C9.679,17.021 10.158,17.217 10.675,17.35L11,20ZM12.05,15.5C13.017,15.5 13.842,15.158 14.525,14.475C15.208,13.792 15.55,12.967 15.55,12C15.55,11.033 15.208,10.208 14.525,9.525C13.842,8.842 13.017,8.5 12.05,8.5C11.067,8.5 10.238,8.842 9.563,9.525C8.888,10.208 8.55,11.033 8.55,12C8.55,12.967 8.888,13.792 9.563,14.475C10.238,15.158 11.067,15.5 12.05,15.5Z"
android:fillColor="#CAD5CA"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,4 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">presentation</string>
<string name="app_name">SKUF Company</string>
<string name="action_login">Sign in</string>
<string name="text_login">Login</string>
<string name="text_password">Password</string>
<string name="greeting_login_description">Sign to your account to continue</string>
<string name="greeting_login">Sign in</string>
<string name="title_profile">Profile</string>
<string name="label_qr_login">Enter by code</string>
<string name="label_card_login">Enter by card</string>
<string name="label_last_visit">Last visit:</string>
<string name="label_visits">Visits</string>
<string name="label_scan">Scan QR</string>
<string name="label_tester">Tester</string>
<string name="label_developer">Developer</string>
<string name="label_designer">Designer</string>
<string name="label_analyst">Analytics</string>
<string name="label_administrator">Administrator</string>
<string name="code_scanned">Code scanned</string>
<string name="code_scanned_success">Success</string>
<string name="code_scanned_error">Enter was cancelled</string>
<string name="code_scanned_warning">Something went wrong</string>
<string name="close">Close</string>
<string name="label_last_visit_none">None</string>
<string name="admin_header">Admin</string>
</resources>

View File

@ -1,10 +1,28 @@
<resources>
<string name="app_name">presentation</string>
<string name="login_button">Войти</string>
<string name="text_email">Почта</string>
<string name="app_name">СКУД</string>
<string name="action_login">Войти</string>
<string name="text_login">Логин</string>
<string name="text_password">Пароль</string>
<string name="greeting_login_description">Войдите в свой аккаунт чтобы продолжить</string>
<string name="greeting_login">Вход</string>
<string name="placholder_email" translatable="false">example@mail.com</string>
<string name="placholder_login" translatable="false">\@pivanov</string>
<string name="placeholder_password" translatable="false">**********</string>
<string name="title_profile">Профиль</string>
<string name="label_qr_login">Вход по коду</string>
<string name="label_card_login">Вход по карте</string>
<string name="label_last_visit">Последний вход:</string>
<string name="label_visits">Посещения</string>
<string name="label_scan">Сканировать код</string>
<string name="label_tester">Тестировщик</string>
<string name="label_developer">Разработчик</string>
<string name="label_designer">Дизайнер</string>
<string name="label_analyst">Аналитик</string>
<string name="label_administrator">Администратор</string>
<string name="code_scanned">Код отсканирован</string>
<string name="code_scanned_success">Успешно</string>
<string name="code_scanned_error">Вход был отменён</string>
<string name="code_scanned_warning">Что-то пошло не так</string>
<string name="close">Закрыть</string>
<string name="admin_header">Управление</string>
<string name="label_last_visit_none">Нет</string>
</resources>