commit 67c19c7edd652261e4b42e91a9c4337a7bb58c43 Author: Universall Date: Tue Feb 18 18:07:32 2025 +0300 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599bc8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.idea/ +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d042a44 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + kotlin("plugin.serialization") version "2.1.10" + id("kotlin-kapt") + id("com.google.dagger.hilt.android").version("2.51.1") +} + +android { + namespace = "com.displaynone.acss" + compileSdk = 35 + + defaultConfig { + applicationId = "com.displaynone.acss" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + val cameraX = "1.3.4" + implementation("androidx.camera:camera-core:$cameraX") + implementation("androidx.camera:camera-camera2:$cameraX") + implementation("androidx.camera:camera-lifecycle:$cameraX") + implementation("androidx.camera:camera-view:$cameraX") + implementation("androidx.camera:camera-mlkit-vision:1.4.0-rc04") + implementation("com.google.mlkit:barcode-scanning:17.3.0") + + val hilt = "2.51.1" + implementation("com.google.dagger:hilt-android:$hilt") + kapt("com.google.dagger:hilt-android-compiler:$hilt") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + implementation("com.google.code.gson:gson:2.8.8") + + implementation("com.github.bumptech.glide:glide:4.16.0") + + implementation("io.ktor:ktor-client-core:3.0.3") + implementation("io.ktor:ktor-client-cio:3.0.3") + implementation("io.ktor:ktor-client-content-negotiation:3.0.3") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3") + + implementation(libs.androidx.core.ktx) + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0") + implementation("androidx.fragment:fragment-ktx:1.6.1") + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.swiperefreshlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.security.crypto) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} +kapt { + correctErrorTypes = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/displaynone/acss/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/displaynone/acss/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..7028cf4 --- /dev/null +++ b/app/src/androidTest/java/com/displaynone/acss/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.displaynone.acss + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.displaynone.acss", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d4d10b0 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/ACSSApplication.kt b/app/src/main/java/com/displaynone/ACSSApplication.kt new file mode 100644 index 0000000..7fff61b --- /dev/null +++ b/app/src/main/java/com/displaynone/ACSSApplication.kt @@ -0,0 +1,11 @@ +package com.displaynone + +import android.app.Application +import com.displaynone.acss.components.auth.models.user.UserServiceST + +class ACSSApplication : Application() { + override fun onCreate() { + super.onCreate() + UserServiceST.createInstance(this) + } +} diff --git a/app/src/main/java/com/displaynone/acss/MainActivity.kt b/app/src/main/java/com/displaynone/acss/MainActivity.kt new file mode 100644 index 0000000..5226b7e --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/MainActivity.kt @@ -0,0 +1,20 @@ +package com.displaynone.acss + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenManager.java b/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenManager.java new file mode 100644 index 0000000..78a6c77 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenManager.java @@ -0,0 +1,69 @@ +package com.displaynone.acss.components.auth.models.user; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.Nullable; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKeys; + + +import java.io.IOException; +import java.security.GeneralSecurityException; + +public class AuthTokenManager { + // Preferences + private static final String _PREFERENCES_FILENAME = "authData"; + private final SharedPreferences _preferences; + + // Keys + private static final String _KEY_ACCESS_TOKEN = "accessToken"; + private static final String _KEY_REFRESH_TOKEN = "refreshToken"; + + // Cache + private AuthTokenPair _tokenPair; + + public AuthTokenManager(Context context) { + this._preferences = this._createEncryptedPreferences(context); + } + + private SharedPreferences _createEncryptedPreferences(Context ctx) { + try { + return EncryptedSharedPreferences.create( + _PREFERENCES_FILENAME, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + ctx, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } + } + + + public void clear() { + _preferences.edit().clear().apply(); + } + + public boolean hasTokens() { + return this.getAuthTokenPair() != null; + } + + public void saveTokens(AuthTokenPair tokenPair) { + this._tokenPair = tokenPair; + _preferences.edit() + .putString(_KEY_ACCESS_TOKEN, tokenPair.getAccessToken()) + .putString(_KEY_REFRESH_TOKEN, tokenPair.getRefreshToken()) + .apply(); + } + + public @Nullable AuthTokenPair getAuthTokenPair() { + if (this._tokenPair != null) return this._tokenPair; + + String accessToken = _preferences.getString(_KEY_ACCESS_TOKEN, null); + String refreshToken = _preferences.getString(_KEY_REFRESH_TOKEN, null); + this._tokenPair = (accessToken != null && refreshToken != null) ? new AuthTokenPair(accessToken, refreshToken) : null; + return this._tokenPair; + } +} diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenPair.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenPair.kt new file mode 100644 index 0000000..7d73fc5 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/AuthTokenPair.kt @@ -0,0 +1,10 @@ +package com.displaynone.acss.components.auth.models.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AuthTokenPair( + @SerialName("accessToken") val accessToken: String, + @SerialName("refreshToken") val refreshToken: String +) diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/TokenManager.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/TokenManager.kt new file mode 100644 index 0000000..722d37b --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/TokenManager.kt @@ -0,0 +1,5 @@ +package com.displaynone.acss.components.auth.models.user + +class TokenManager { + //TODO() +} diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserEntity.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserEntity.kt new file mode 100644 index 0000000..b71e508 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserEntity.kt @@ -0,0 +1,10 @@ +package com.displaynone.acss.components.auth.models.user + +data class UserEntity( + val id: Long, + val login: String, + val name: String, + val photo: String, + val position: String, + val lastVisit: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserMapper.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserMapper.kt new file mode 100644 index 0000000..75be2eb --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserMapper.kt @@ -0,0 +1,29 @@ +package com.displaynone.acss.components.auth.models.user + +import com.displaynone.acss.components.auth.models.user.repository.dto.UserDTO + +class UserMapper { + + fun fromDto(userDTO: UserDTO): UserEntity { + val userEntity = UserEntity( + id = userDTO.id, + login = userDTO.login, + position = userDTO.position, + name = userDTO.name, + lastVisit = userDTO.lastVisit, + photo = userDTO.photo + ) + return userEntity + } + fun toDTO(userEntity: UserEntity): UserDTO { + val userDto = UserDTO( + id = userEntity.id, + login = userEntity.login, + name = userEntity.name, + photo = userEntity.photo, + position = userEntity.position, + lastVisit = userEntity.lastVisit, + ) + return userDto + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserServiceST.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserServiceST.kt new file mode 100644 index 0000000..2172191 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/UserServiceST.kt @@ -0,0 +1,46 @@ +package com.displaynone.acss.components.auth.models.user + +import android.content.Context +import com.displaynone.acss.components.auth.models.user.repository.UserRepository +import com.displaynone.acss.components.auth.models.user.repository.dto.UserDTO + + +class UserServiceST( + private val tokenManager: AuthTokenManager, +) { + private val userRepository: UserRepository = UserRepository() + + companion object { + private var instance: UserServiceST? = null + + fun createInstance(context: Context) { + if (instance == null) { + val tokenManager = AuthTokenManager(context) + instance = UserServiceST(tokenManager) + } + } + + fun getInstance(): UserServiceST { + return instance ?: throw RuntimeException("null instance") + } + } + suspend fun login(login: String): Result{ + return runCatching { + userRepository.login(login = login).getOrThrow().let { data -> + tokenManager.saveTokens(data) + } + } + } + fun logout(){ + tokenManager.clear() + } + suspend fun getInfo(): Result{ + if (!tokenManager.hasTokens()) { + throw RuntimeException("access token is null") + } + return userRepository.getInfo(tokenManager.authTokenPair!!.accessToken) + } + suspend fun openDoor(code: String): Result { + return userRepository.openDoor(tokenManager.authTokenPair!!.accessToken, code = code) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/UserRepository.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/UserRepository.kt new file mode 100644 index 0000000..3ba045f --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/UserRepository.kt @@ -0,0 +1,125 @@ +package com.displaynone.acss.components.auth.models.user.repository + +import android.util.Log +import com.displaynone.acss.components.auth.models.user.AuthTokenManager +import com.displaynone.acss.components.auth.models.user.AuthTokenPair +import com.displaynone.acss.config.Constants.serverUrl +import com.displaynone.acss.config.Network +import com.displaynone.acss.components.auth.models.user.repository.dto.RegisterUserDto +import com.displaynone.acss.components.auth.models.user.repository.dto.UserDTO +import com.google.gson.Gson +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.patch +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText + +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.encodeURLPath +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlin.math.log + +class UserRepository( + +) { + suspend fun isUserExist(login: String): Result = withContext(Dispatchers.IO) { + runCatching { + val encodedLogin = login.encodeURLPath() + val result = Network.client.get("$serverUrl/api/$encodedLogin/auth/") + result.status != HttpStatusCode.OK + } + } + suspend fun login(login: String): Result = withContext(Dispatchers.IO) { + runCatching { + val result = Network.client.post("$serverUrl/api/auth/login") { + headers { + append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + } + setBody("""{ "login": "$login" }""") + } + if (result.status != HttpStatusCode.OK) { + error("Status ${result.status}: ${result.body()}") + } +// val gson = Gson() +// val tokenPair = gson.fromJson(result.bodyAsText(), AuthTokenPair::class.java) + Log.d("UserRepository", result.bodyAsText()) +// result.body() + val tokenPair = Json.decodeFromString(result.bodyAsText()) + Result.success(tokenPair) + }.getOrElse { exception -> + Result.failure(exception) + } + } + suspend fun openDoor(token: String, code: String): Result = withContext(Dispatchers.IO){ + runCatching{ + val result = Network.client.post("$serverUrl/api/opendoor") { + headers { + append(HttpHeaders.Authorization, "Bearer $token") + } + setBody("""{ "code": "$code" }""") + } + if (result.status != HttpStatusCode.OK) { + error("Status ${result.status}: ${result.body()}") + } + Log.d("UserRepository", result.bodyAsText()) + result.status.value + } + } + suspend fun getInfo(token: String): Result = withContext(Dispatchers.IO){ + runCatching { + val result = Network.client.get("$serverUrl/api/users/me") { + headers { + append(HttpHeaders.Authorization, "Bearer $token") + } + } + if (result.status != HttpStatusCode.OK) { + error("Status ${result.status}: ${result.body()}") + } + Log.d("UserRepository", result.bodyAsText()) + result.body() + } + } + suspend fun openDoor(token: String, code: Long): Result = withContext(Dispatchers.IO) { + runCatching { + val result = Network.client.patch("$serverUrl/api/open") { + headers { + append(HttpHeaders.Authorization, token) + } + setBody("""{"value":$code}""") + } + if (result.status != HttpStatusCode.OK) { + error("Status ${result.status}: ${result.body()}") + } + Unit + } + } + + suspend fun register(login: String, password: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val result = Network.client.post("$serverUrl/api/person/register") { + contentType(ContentType.Application.Json) + setBody( + RegisterUserDto( + login = login, + password = password, + ) + ) + } + if (result.status != HttpStatusCode.Created) { + Log.w("UserRepository", "Status: ${result.status}, Body: ${result.body()}") + error("Status ${result.status}: ${result.body()}") + } + Unit + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/RegisterUserDto.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/RegisterUserDto.kt new file mode 100644 index 0000000..2b6a751 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/RegisterUserDto.kt @@ -0,0 +1,7 @@ +package com.displaynone.acss.components.auth.models.user.repository.dto + +data class RegisterUserDto( + val login: String, + val password: String, +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/UserDTO.kt b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/UserDTO.kt new file mode 100644 index 0000000..a913f59 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/components/auth/models/user/repository/dto/UserDTO.kt @@ -0,0 +1,26 @@ +package com.displaynone.acss.components.auth.models.user.repository.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.util.Date + +@Serializable +data class UserDTO ( + @SerialName("id") + val id: Long, + + @SerialName("login") + val login: String, + + @SerialName("name") + val name: String, + + @SerialName("photo") + val photo: String, + + @SerialName("position") + val position: String, + + @SerialName("lastVisit") + val lastVisit: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/config/Constants.kt b/app/src/main/java/com/displaynone/acss/config/Constants.kt new file mode 100644 index 0000000..43646d9 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/config/Constants.kt @@ -0,0 +1,5 @@ +package com.displaynone.acss.config + +object Constants { + const val serverUrl = "http://192.168.1.107:8080" +} diff --git a/app/src/main/java/com/displaynone/acss/config/Network.kt b/app/src/main/java/com/displaynone/acss/config/Network.kt new file mode 100644 index 0000000..ecb110f --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/config/Network.kt @@ -0,0 +1,18 @@ +package com.displaynone.acss.config + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +object Network { + val client = HttpClient(CIO) { + install(ContentNegotiation){ + json(Json{ + isLenient = true + ignoreUnknownKeys = true + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/auth/AuthFragment.kt b/app/src/main/java/com/displaynone/acss/ui/auth/AuthFragment.kt new file mode 100644 index 0000000..823bc73 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/auth/AuthFragment.kt @@ -0,0 +1,87 @@ +package com.displaynone.acss.ui.auth + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.displaynone.acss.R +import com.displaynone.acss.databinding.FragmentAuthBinding +import com.displaynone.acss.util.collectWithLifecycle +import com.displaynone.acss.util.navigateTo +import com.displaynone.acss.ui.auth.AuthViewModel.Action + +class AuthFragment: Fragment(R.layout.fragment_auth) { + private var _binding: FragmentAuthBinding? = null + private val binding: FragmentAuthBinding get() = _binding!! + + private val viewModel: AuthViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentAuthBinding.bind(view) + setupLoginButton() + + viewModel.action.collectWithLifecycle(this) { action -> + when (action) { + Action.GotoProfile -> navigateTo(view, R.id.action_authFragment_to_profileFragment) + } + } + viewModel.errorState.collectWithLifecycle(this) { errorMessage -> + errorMessage?.let { + Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() + binding.errorTitle.text = errorMessage + binding.errorTitle.visibility = View.VISIBLE + } + } + binding.next.setOnClickListener{ + onLoginButtonClicked(view) + } + } + + + private fun setupLoginButton() { + binding.login.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + @SuppressLint("ResourceAsColor") + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.error.visibility = View.GONE + val username = s.toString() + val valid = isUsernameValid(username) + binding.hint.visibility = if(valid) View.INVISIBLE else View.VISIBLE + binding.next.isEnabled = valid + val color = if (valid) R.color.primary else R.color.secondary + binding.next.backgroundTintList = ContextCompat.getColorStateList(requireContext(), color) + } + + override fun afterTextChanged(s: Editable?) {} + }) + } + + private fun isUsernameValid(username: String): Boolean { + val alf = "^[a-zA-Z0-9_]+$".toRegex() + return username.isNotEmpty() && + username.length >= 3 && + !username[0].isDigit() && + alf.matches(username) + } +// private fun subscribe() { +// viewModel.state.collectWhenStarted(this) { state -> +// binding.login.setOnClickListener(this::onLoginButtonClicked) +// } +// } + + private fun onLoginButtonClicked(view: View) { + val login = binding.login.text.toString() + if (login.isEmpty()) return + + viewModel.login(login) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/auth/AuthViewModel.kt b/app/src/main/java/com/displaynone/acss/ui/auth/AuthViewModel.kt new file mode 100644 index 0000000..0426f7e --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/auth/AuthViewModel.kt @@ -0,0 +1,51 @@ +package com.displaynone.acss.ui.auth + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.displaynone.acss.components.auth.models.user.UserServiceST +import com.displaynone.acss.components.auth.models.user.repository.dto.RegisterUserDto +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +class AuthViewModel(): ViewModel() { + private val _action = Channel( + capacity = Channel.BUFFERED, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val action = _action.receiveAsFlow() + private val _errorState = MutableStateFlow(null) + val errorState: StateFlow = _errorState.asStateFlow() + + fun login(login: String){ + viewModelScope.launch { + try { + UserServiceST.getInstance().login(login).fold( + onSuccess = { openProfile() }, + onFailure = { error -> + Log.e("AuthViewModel", "Login failed: ${error.message ?: "Unknown error"}") + _errorState.value = error.message ?: "Ошибка входа" + } + ) + } catch (e: Exception) { + Log.e("AuthViewModel", "Connection error: ${e.message}") + _errorState.value = "Проблемы с подключением. Повторите попытку позже." + } + } + } + private fun openProfile() { + viewModelScope.launch { + _action.send(Action.GotoProfile) + } + } + + sealed interface Action { + data object GotoProfile : Action + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/profile/ProfileFragment.kt b/app/src/main/java/com/displaynone/acss/ui/profile/ProfileFragment.kt new file mode 100644 index 0000000..0e35309 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/profile/ProfileFragment.kt @@ -0,0 +1,112 @@ +package com.displaynone.acss.ui.profile + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.bumptech.glide.Glide +import com.displaynone.acss.R +import com.displaynone.acss.components.auth.models.user.repository.dto.UserDTO +import com.displaynone.acss.databinding.FragmentProfileBinding +import com.displaynone.acss.ui.profile.ProfileViewModel.Action +import com.displaynone.acss.ui.scan.QrScanDestination +import com.displaynone.acss.util.collectWithLifecycle +import com.displaynone.acss.util.navigateTo + +class ProfileFragment: Fragment(R.layout.fragment_profile) { + private var _binding: FragmentProfileBinding? = null + private val binding: FragmentProfileBinding get() = _binding!! + + private val viewModel: ProfileViewModel by viewModels() + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentProfileBinding.bind(view) + + binding.swipeRefresh.setOnRefreshListener { + refreshData() + } + binding.logout.setOnClickListener{ + logout() + } + binding.scan.setOnClickListener{ + viewModel.openScan() + } + subscribe() + refreshData() + waitForQRScanResult() + } + + private fun refreshData() { + Log.d("ProfileFragment", "Refreshed") + viewModel.getInfo() + subscribeToGetData() + } + fun subscribe() { + viewModel.action.collectWithLifecycle(this) { action -> + if (action is Action.GoToAuth) { + view?.let { navigateTo(it, R.id.action_profileFragment_to_authFragment) } ?: throw IllegalStateException("View is null") + } + if (action is Action.GoToScan) { + view?.let { navigateTo(it, R.id.action_profileFragment_to_qrScanFragment) }?: throw IllegalStateException("View is null") + } + } + } + private fun waitForQRScanResult() { + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + + } + }) + + findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData( + QrScanDestination.REQUEST_KEY + )?.observe(viewLifecycleOwner) { bundle -> + val qrCode = bundle.getString("key_qr") + if (!qrCode.isNullOrEmpty()) view?.let { + val bundle = Bundle().apply { + putString("qrCode", qrCode) + } + navigateTo(it, R.id.action_profileFragment_to_qrResultFragment, bundle) + } + } + } + private fun logout() { + viewModel.logout() + viewModel.openAuth() + Toast.makeText(activity, "LOGOUT", Toast.LENGTH_SHORT).show() + } + + private fun subscribeToGetData(){ + viewModel.state.collectWithLifecycle(this) { state -> + if (state is ProfileViewModel.State.Show) { + val userDto: UserDTO = state.item + binding.fio.text = userDto.name + binding.position.text = userDto.position + binding.lastEntry.text = userDto.lastVisit + setAvatar(userDto.photo) + } + } + } + + private fun setAvatar(photo: String) { + Glide.with(requireContext()) + .load(photo) + .placeholder(R.drawable.ic_photo) + .error(R.drawable.ic_back) + .into(binding.avatar) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/displaynone/acss/ui/profile/ProfileViewModel.kt new file mode 100644 index 0000000..573e354 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/profile/ProfileViewModel.kt @@ -0,0 +1,61 @@ +package com.displaynone.acss.ui.profile + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.displaynone.acss.components.auth.models.user.UserServiceST +import com.displaynone.acss.components.auth.models.user.repository.dto.UserDTO +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +class ProfileViewModel(): ViewModel() { + private val _action = Channel( + capacity = Channel.BUFFERED, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val action = _action.receiveAsFlow() + val _state = MutableStateFlow(State.Loading) + val state = _state.asStateFlow() + + + fun logout(){ + UserServiceST.getInstance().logout() + } + fun getInfo(){ + viewModelScope.launch { + UserServiceST.getInstance().getInfo().fold( + onSuccess = { data -> + _state.emit(State.Show(data)) + }, + onFailure = { error -> + error.message?.let { error(it) } + Log.e("ProfileViewModel", error.message.toString()) + } + ) + } + } + fun openAuth(){ + viewModelScope.launch { + _action.send(Action.GoToAuth) + } + } + fun openScan(){ + viewModelScope.launch { + _action.send(Action.GoToScan) + } + } + sealed interface State { + data object Loading : State + data class Show( + val item: UserDTO + ) : State + } + sealed interface Action { + data object GoToAuth: Action + data object GoToScan: Action + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/result/QrResultFragment.kt b/app/src/main/java/com/displaynone/acss/ui/result/QrResultFragment.kt new file mode 100644 index 0000000..4f47f45 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/result/QrResultFragment.kt @@ -0,0 +1,74 @@ +package com.displaynone.acss.ui.result + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.displaynone.acss.R +import com.displaynone.acss.databinding.FragmentQrResultBinding +import com.displaynone.acss.util.collectWithLifecycle +import com.displaynone.acss.util.navigateTo + + +class QrResultFragment : Fragment(R.layout.fragment_qr_result) { + private var _binding: FragmentQrResultBinding? = null + private val binding: FragmentQrResultBinding get() = _binding!! + private val viewModel: QrResultViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Log.d("QrResultFragment", getQrCode()) + _binding = FragmentQrResultBinding.bind(view) + binding.close.setOnClickListener(this::closeQrScanFragment) + + this.openDoor() + + viewModel.state.collectWithLifecycle(this){ state -> + if (state is QrResultViewModel.State.Result){ + if (state.resultCode == 200) { + setResult(getString(R.string.success)) + } else if (state.resultCode == 400) { + setResult(getString(R.string.wrong)) + } else if (state.resultCode == 401) { + setResult(getString(R.string.cancel)) + } + } + if (state is QrResultViewModel.State.Error){ + setResult(state.errorMessage) + } + } + } + + private fun openDoor() { + + val qrCodeValueLong: Long + + try { + qrCodeValueLong = getQrCode().toLong() + } catch (exception: Exception) { + when (exception) { + is NumberFormatException, is IllegalArgumentException -> setResult(getString(R.string.wrong)) + else -> throw exception + } + return + } + + viewModel.openDoor(qrCodeValueLong) + } + + private fun getQrCode(): String { + return arguments?.getString("qrCode") ?: "No QR Code Provided" + } + + private fun closeQrScanFragment(view: View) { + navigateTo(view, R.id.action_qrResultFragment_to_profileFragment) + } + + private fun setResult(result: String) { + binding.result.text = result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/result/QrResultViewModel.kt b/app/src/main/java/com/displaynone/acss/ui/result/QrResultViewModel.kt new file mode 100644 index 0000000..93f6fc5 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/result/QrResultViewModel.kt @@ -0,0 +1,49 @@ +package com.displaynone.acss.ui.result +import androidx.lifecycle.AndroidViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import android.app.Application +import android.net.http.HttpException +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.displaynone.acss.components.auth.models.user.UserServiceST +import kotlinx.coroutines.launch +import kotlinx.io.IOException + +class QrResultViewModel(application: Application) : AndroidViewModel(application) { + val _state = MutableStateFlow(State.Result(-1)) + val state = _state.asStateFlow() + + fun openDoor(code: Long) { + viewModelScope.launch { + try { + val stringCode = code.toString() + UserServiceST.getInstance().openDoor(stringCode).fold( + onSuccess = { response -> + Log.d("QrResultViewModel", "Door opened") + _state.emit(State.Result( + response + )) + }, + onFailure = { error -> + Log.e("QrResultViewModel", "Door open failed: ${error.message ?: "Unknown error"}") + _state.emit(State.Error( + error.message.toString() + )) + } + ) + } catch (e: Exception) { + Log.e("QrResultViewModel", "Unexpected error: ${e.message}") + } + } + } + + sealed interface State { + data class Result( + val resultCode: Int + ): State + data class Error( + val errorMessage: String + ): State + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/scan/QrScanDestination.kt b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanDestination.kt new file mode 100644 index 0000000..62d11e8 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanDestination.kt @@ -0,0 +1,29 @@ +package com.displaynone.acss.ui.scan + +import android.os.Bundle +import androidx.core.os.bundleOf +import kotlinx.serialization.Serializable + +@Serializable +data object QrScanDestination { + const val REQUEST_KEY = "qr_result" + private const val KEY_QR_DATA = "key_qr" + + fun newInstance(): QrScanFragment { + return QrScanFragment() + } + + fun getDataIfExist(bundle: Bundle): String? { + return if (bundle.containsKey(KEY_QR_DATA)) { + bundle.getString(KEY_QR_DATA) + } else { + null + } + } + + internal fun packToBundle(data: String): Bundle { + return bundleOf( + KEY_QR_DATA to data + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/scan/QrScanFragment.kt b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanFragment.kt new file mode 100644 index 0000000..b1f3c2a --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanFragment.kt @@ -0,0 +1,139 @@ +package com.displaynone.acss.ui.scan + +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.ImageAnalysis +import androidx.camera.mlkit.vision.MlKitAnalyzer +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.displaynone.acss.R +import com.displaynone.acss.databinding.FragmentQrScanBinding +import com.displaynone.acss.util.collectWhenStarted +import com.displaynone.acss.util.visibleOrGone +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode + + +class QrScanFragment : Fragment(R.layout.fragment_qr_scan) { + private var _binding: FragmentQrScanBinding? = null + private val binding: FragmentQrScanBinding get() = _binding!! + + private var barcodeScanner: BarcodeScanner? = null + private var isCameraInit: Boolean = false + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> viewModel.onPermissionResult(isGranted) } + + private val viewModel: QrScanViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentQrScanBinding.bind(view) + sendResult(bundleOf()) + subscribe() + initCallback() + } + + private fun initCallback() { + binding.close.setOnClickListener { viewModel.close() } + } + + private fun subscribe() { + viewModel.state.collectWhenStarted(this) { state -> + binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading) + binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan) + if (!isCameraInit && state is QrScanViewModel.State.Scan) { + startCamera() + isCameraInit = true + } + } + + viewModel.action.collectWhenStarted(this) { action -> + when (action) { + is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission) + is QrScanViewModel.Action.CloseWithCancel -> { + goBack() + } + is QrScanViewModel.Action.CloseWithResult -> { + sendResult(QrScanDestination.packToBundle(action.result)) + goBack() + } + } + } + } + + private fun requestPermission(permission: String) { + permissionLauncher.launch(permission) + } + + private fun startCamera() { + val context = requireContext() + val cameraController = LifecycleCameraController(context) + val previewView: PreviewView = binding.viewFinder + val executor = ContextCompat.getMainExecutor(context) + + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + val barcodeScanner = BarcodeScanning.getClient(options) + this.barcodeScanner = barcodeScanner + + cameraController.setImageAnalysisAnalyzer( + executor, + MlKitAnalyzer( + listOf(barcodeScanner), + ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, + executor + ) { result -> + result?.getValue(barcodeScanner)?.firstOrNull()?.let { value -> + viewModel.findBarcode(value) + + } + } + ) + + cameraController.bindToLifecycle(this) + previewView.controller = cameraController + } + + override fun onDestroyView() { + barcodeScanner?.close() + barcodeScanner = null + _binding = null + super.onDestroyView() + } + + private fun goBack() { + findNavControllerOrNull()?.popBackStack() + ?: requireActivity().onBackPressedDispatcher.onBackPressed() + } + + private fun sendResult(bundle: Bundle) { + setFragmentResult( + QrScanDestination.REQUEST_KEY, + bundle + ) + findNavControllerOrNull() + ?.previousBackStackEntry + ?.savedStateHandle + ?.set(QrScanDestination.REQUEST_KEY, bundle) + } + + private fun findNavControllerOrNull(): NavController? { + return try { + findNavController() + } catch (_: Throwable) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/ui/scan/QrScanViewModel.kt b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanViewModel.kt new file mode 100644 index 0000000..6961085 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/ui/scan/QrScanViewModel.kt @@ -0,0 +1,94 @@ +package com.displaynone.acss.ui.scan + +import androidx.lifecycle.ViewModel + +import android.Manifest +import android.app.Application +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.displaynone.acss.util.MutablePublishFlow +import com.google.mlkit.vision.barcode.common.Barcode +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class QrScanViewModel( + application: Application +) : AndroidViewModel(application) { + + private val _action = MutablePublishFlow() + val action = _action.asSharedFlow() + + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + init { + checkPermission() + } + + fun onPermissionResult(isGranted: Boolean) { + viewModelScope.launch { + if (isGranted) { + _state.update { State.Scan } + } else { + _action.emit(Action.CloseWithCancel) + } + } + } + + private fun checkPermission() { + viewModelScope.launch { + val isPermissionGranted = ContextCompat.checkSelfPermission( + getApplication(), + CAMERA_PERMISSION + ) == PackageManager.PERMISSION_GRANTED + if (isPermissionGranted) { + _state.update { State.Scan } + } else { + delay(1000) + _action.emit(Action.RequestPermission(CAMERA_PERMISSION)) + } + } + } + + fun findBarcode(barcode: Barcode) { + viewModelScope.launch { + barcode.rawValue?.let { value -> + _action.emit(Action.CloseWithResult(value)) + } + } + } + + fun close() { + viewModelScope.launch { + _action.emit(Action.CloseWithCancel) + } + } + + sealed interface State { + data object Loading : State + + data object Scan : State + } + + sealed interface Action { + data class RequestPermission( + val permission: String + ) : Action + data object CloseWithCancel : Action + data class CloseWithResult( + val result: String + ) : Action + } + + private companion object { + val initialState = State.Loading + + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/util/FlowExtensions.kt b/app/src/main/java/com/displaynone/acss/util/FlowExtensions.kt new file mode 100644 index 0000000..53562f0 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/util/FlowExtensions.kt @@ -0,0 +1,4 @@ +package com.displaynone.acss.util + +class FlowExtensions { +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/util/FragmentExtensions.kt b/app/src/main/java/com/displaynone/acss/util/FragmentExtensions.kt new file mode 100644 index 0000000..25a7a4a --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/util/FragmentExtensions.kt @@ -0,0 +1,29 @@ +package com.displaynone.acss.util + +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import androidx.lifecycle.flowWithLifecycle +fun Flow.collectWithLifecycle( + fragment: Fragment, + function: suspend (T) -> Unit +) { + fragment.viewLifecycleOwner.lifecycleScope.launch { + fragment.repeatOnLifecycle(Lifecycle.State.STARTED){ + collect{ function.invoke(it) } + } + } +} +inline fun Flow.collectWhenStarted( + fragment: Fragment, + crossinline collector: (T) -> Unit +) { + fragment.viewLifecycleOwner.lifecycleScope.launch { + flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> + collector.invoke(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/util/GlobalUtils.kt b/app/src/main/java/com/displaynone/acss/util/GlobalUtils.kt new file mode 100644 index 0000000..901aae4 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/util/GlobalUtils.kt @@ -0,0 +1,22 @@ +package com.displaynone.acss.util + +import android.os.Bundle +import android.view.View +import androidx.annotation.IdRes +import androidx.navigation.NavController +import androidx.navigation.Navigation + +fun navigateTo(view: View, @IdRes actionId: Int) { + Navigation.findNavController(view).navigate(actionId) +} + +fun navigateTo(navController: NavController, @IdRes actionId: Int) { + navController.navigate(actionId) +} +fun navigateTo(view: View, @IdRes actionId: Int, bundle: Bundle) { + Navigation.findNavController(view).navigate(actionId, bundle) +} + +fun navigateTo(navController: NavController, @IdRes actionId: Int, bundle: Bundle) { + navController.navigate(actionId, bundle) +} \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/util/MutablePublishFlow.kt b/app/src/main/java/com/displaynone/acss/util/MutablePublishFlow.kt new file mode 100644 index 0000000..549fc16 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/util/MutablePublishFlow.kt @@ -0,0 +1,10 @@ +package com.displaynone.acss.util + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow + +fun MutablePublishFlow() = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + BufferOverflow.DROP_OLDEST +) \ No newline at end of file diff --git a/app/src/main/java/com/displaynone/acss/util/ViewExtensions.kt b/app/src/main/java/com/displaynone/acss/util/ViewExtensions.kt new file mode 100644 index 0000000..3d33911 --- /dev/null +++ b/app/src/main/java/com/displaynone/acss/util/ViewExtensions.kt @@ -0,0 +1,6 @@ +package com.displaynone.acss.util +import android.view.View + +fun View.visibleOrGone(isVisible: Boolean) { + this.visibility = if (isVisible) View.VISIBLE else View.GONE +} \ No newline at end of file diff --git a/app/src/main/res/color/button_background.xml b/app/src/main/res/color/button_background.xml new file mode 100644 index 0000000..d3fbddf --- /dev/null +++ b/app/src/main/res/color/button_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..0d5fd23 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..e4a5389 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..8a2bdbf --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_photo.xml b/app/src/main/res/drawable/ic_photo.xml new file mode 100644 index 0000000..c3917d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..3d0ca35 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_auth.xml b/app/src/main/res/layout/fragment_auth.xml new file mode 100644 index 0000000..e4b15e3 --- /dev/null +++ b/app/src/main/res/layout/fragment_auth.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..414ce46 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_qr_result.xml b/app/src/main/res/layout/fragment_qr_result.xml new file mode 100644 index 0000000..e754177 --- /dev/null +++ b/app/src/main/res/layout/fragment_qr_result.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_qr_scan.xml b/app/src/main/res/layout/fragment_qr_scan.xml new file mode 100644 index 0000000..a52eb71 --- /dev/null +++ b/app/src/main/res/layout/fragment_qr_scan.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..453aa8f --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a48ad0d --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c0f1af1 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,18 @@ + + + #FF000000 + #FFFFFFFF + #03A9F4 + #6c757d + #ffc107 + #dc3545 + #198754 + #0dcaf0 + #17a2b8 + #f8f9fa + #343a40 + #6610f2 + #808080 + #FF9800 + #FF1C1C + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c49ebaf --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + ACSS + Enter Login + Enter password + Hello + Continue + logout + Login must be at least 3 characters long,\nstart with a letter,\ncontain only letters and numbers. + Close + Scan + Вход был отменён + Успешно + Что-то пошло не так + Логина не существует или неверный + ОК + Error + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..8040f1a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +