working on network and UI

This commit is contained in:
leca 2024-10-29 03:28:26 +03:00
parent 3d4f86085e
commit a0d96da9e4
15 changed files with 442 additions and 102 deletions

View File

@ -50,13 +50,16 @@ dependencies {
implementation(libs.androidx.activity)
implementation(libs.androidx.legacy.support.v4)
implementation(libs.androidx.fragment)
implementation(libs.androidx.material3.android)
testImplementation(libs.junit)
implementation(libs.volley)
androidTestImplementation(libs.androidx.junit)
implementation(libs.zxing.android.embedded)
implementation("com.google.zxing:core:3.4.1")
androidTestImplementation(libs.androidx.espresso.core)
implementation("com.google.android.material:material:1.3.0-alpha03")
implementation(libs.androidx.security.crypto)
implementation(libs.okhttp)
// Barcode scanning API
implementation (libs.barcode.scanning)

View File

@ -48,6 +48,14 @@
android:name=".activities.FindBarcodelessAbstractProduct"
android:exported="false"
android:theme="@style/Theme.BarcodeScannerForEmployees"/>
<activity
android:name=".activities.MainActivity"
android:exported="false"
android:theme="@style/Theme.BarcodeScannerForEmployees"/>
<activity
android:name=".activities.LoginActivity"
android:exported="false"
android:theme="@style/Theme.BarcodeScannerForEmployees"/>
<activity
android:name=".activities.FullscreenActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
@ -70,7 +78,7 @@
</provider>
<activity
android:name=".activities.MainActivity"
android:name=".activities.NavigatorActivity"
android:exported="true"
android:theme="@style/Theme.BarcodeScannerForEmployees">
<intent-filter>

View File

@ -0,0 +1,127 @@
package org.foxarmy.barcodescannerforemployees
import android.util.Log
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.asRequestBody
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import java.io.File
import kotlin.concurrent.thread
class Net {
fun requestProductFromOnlineDB(barcode: String): String {
var response = ""
thread {
val url = "https://ean-online.ru/match.php"
val client = OkHttpClient()
val formBody = FormBody.Builder()
formBody.add("barcode", barcode)
val body = formBody.build()
val request = Request.Builder()
.url(url)
.post(body)
.addHeader("referer", "https://ean-online.ru")
.build()
response = client.newCall(request).execute().body!!.string()
}.join()
return if (response == "") {
"Not found 404"
} else {
response
}
}
fun registerAccount(server: String, username: String, password: String): String {
var token = ""
lateinit var response: Response
thread {
val client = OkHttpClient()
val formBody = FormBody.Builder()
formBody.add("username", username)
formBody.add("password", password)
val body = formBody.build()
val request = Request.Builder()
.url("https://$server/api/user/register")
.post(body)
.addHeader("content-type", "application/x-www-form-urlencoded")
.build()
response = client.newCall(request).execute()
}.join()
return when (response.code) {
200 -> {
login(server, username, password)
}
400 -> {
"Such username exists"
}
else -> {
"Unknown error"
}
}
}
fun login(server: String, username: String, password: String): String {
lateinit var response: Response
thread {
val client = OkHttpClient()
val formBody = FormBody.Builder()
formBody.add("username", username)
formBody.add("password", password)
val body = formBody.build()
val requestLogin = Request.Builder()
.url("https://$server/api/user/login")
.post(body)
.addHeader("content-type", "application/x-www-form-urlencoded")
.build()
response = client.newCall(requestLogin).execute()
}.join()
return response.body!!.string()
}
fun uploadAbstractProduct(server: String, groupId: Int, abstractProduct: AbstractProduct, imageFile: File, token: String): String {
lateinit var response: Response
thread {
val client = OkHttpClient()
val body = MultipartBody.Builder()
body.setType("multipart/form-data".toMediaType())
body.addFormDataPart("file", imageFile.name, imageFile.asRequestBody("image/png".toMediaTypeOrNull()))
body.addFormDataPart("groupId", groupId.toString())
body.addFormDataPart("localId", abstractProduct.id.toString())
body.addFormDataPart("barcode", abstractProduct.barcode)
body.addFormDataPart("name", abstractProduct.name)
body.addFormDataPart("net_weight", abstractProduct.netWeight.toString())
body.addFormDataPart("image_filename", abstractProduct.imageHash)
body.addFormDataPart("category", abstractProduct.category.toString())
body.addFormDataPart("unit", abstractProduct.unit.toString())
val requestBody = body.build()
val request = Request.Builder()
.url("https://$server/api/abstractproduct/create")
.post(requestBody)
.addHeader("Authorization", "Bearer $token")
.build()
response = client.newCall(request).execute()
}.join()
val responseText = response.body!!.string()
return responseText
}
}

View File

@ -1,40 +0,0 @@
package org.foxarmy.barcodescannerforemployees
import android.content.Context
import android.widget.Toast
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
class Requester constructor(var siteName: String, var endpoint: String) {
var response = ""
fun request(context: Context, barcode: String) {
val url = "${siteName}/${endpoint}"
val volleyQueue = Volley.newRequestQueue(context)
val stringRequest = object: StringRequest(
Method.POST, url, { resp ->
run {
response =
if (resp == "") {
"Not found 404"
} else {
resp
}
}
},
{
Toast.makeText(context, "Cannot make request", Toast.LENGTH_LONG).show()
}
) {
override fun getHeaders(): Map<String, String> {
return mapOf("referer" to "$siteName/")
}
public override fun getParams(): MutableMap<String, String> {
return mutableMapOf("barcode" to barcode)
}
}
volleyQueue.add(stringRequest)
}
}

View File

@ -14,15 +14,19 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
import okhttp3.internal.http.hasBody
import org.foxarmy.barcodescannerforemployees.*
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import kotlin.concurrent.thread
import kotlin.math.abs
class AddAbstractProductActivity : AppCompatActivity() {
private lateinit var imageView: ImageView
@ -147,7 +151,21 @@ class AddAbstractProductActivity : AppCompatActivity() {
arrayOf(abstractProduct!!.id.toString())
)
} else if (action == "new" || action == "new_from_barcode"){
db.insert(AbstractProductContract.AbstractProductEntry.TABLE_NAME, null, values)
val id = db.insert(AbstractProductContract.AbstractProductEntry.TABLE_NAME, null, values)
val n = Net()
val abstractProduct = AbstractProduct(id.toInt(), barcode, productName, netWeight.toString().toDouble(), pictureFile.nameWithoutExtension, categorySpinner.selectedItemPosition, unitTypeSpinner.selectedItemPosition)
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val sharedPreferences = EncryptedSharedPreferences.create(
// passing a file name to share a preferences
"sensitive",
masterKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val token = sharedPreferences.getString("token", "")
val response = n.uploadAbstractProduct("bsfe.foxarmy.org", 1, abstractProduct, File(pictureFile.absolutePath), token!!);
}
finish()
@ -165,8 +183,10 @@ class AddAbstractProductActivity : AppCompatActivity() {
fun performRequest(barcode: String) {
barcodeText.setText(this.barcode)
val requester = Requester("https://ean-online.ru", "match.php")
requester.request(this, barcode)
val net = Net();
val result = net.requestProductFromOnlineDB(barcode)
Log.d("QWERTYUIOP", "Result of request: $result")
var abstractProduct: AbstractProduct
if (DBStorageController(this).findAbstractProductByBarcode(
@ -194,10 +214,7 @@ class AddAbstractProductActivity : AppCompatActivity() {
}
thread {
// Я сам в ахуях какой это костыль, пока хз как фиксить, потом придумаю :))
while (requester.response == "") {
}
if (requester.response == "Not found 404") {
if (result == "Not found 404") {
runOnUiThread {
Toast.makeText(this, getString(R.string.no_product_in_online_database), Toast.LENGTH_LONG)
.show()
@ -207,8 +224,7 @@ class AddAbstractProductActivity : AppCompatActivity() {
return@thread
}
abstractProduct = Parser().parse(requester.response)
requester.response = ""
abstractProduct = Parser().parse(result)
runOnUiThread {
productNameText.text = abstractProduct.name
netWeightText.text = abstractProduct.netWeight.toString()

View File

@ -0,0 +1,76 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import org.foxarmy.barcodescannerforemployees.Net
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater);
setContentView(binding.root)
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val sharedPreferences = EncryptedSharedPreferences.create(
// passing a file name to share a preferences
"sensitive",
masterKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
binding.loginButton.setOnClickListener {
val server = binding.serverTextEdit.text.toString()
val username = binding.usernameTextEdit.text.toString()
val password = binding.passwordTextEdit.text.toString()
val n = Net()
val response = n.login(server, username, password)
if (response == "Wrong password") {
Toast.makeText(this, getString(R.string.wrong_password), Toast.LENGTH_SHORT).show()
} else {
sharedPreferences.edit().putString("token", response).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
binding.registerButton.setOnClickListener {
val server = binding.serverTextEdit.text.toString()
val username = binding.usernameTextEdit.text.toString()
val password = binding.passwordTextEdit.text.toString()
val n = Net()
val response = n.registerAccount(server, username, password);
if (response == "Such username exists") {
Toast.makeText(this, getString(R.string.username_already_exists), Toast.LENGTH_SHORT).show();
} else {
sharedPreferences.edit().putString("token", response).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
}
}

View File

@ -69,6 +69,7 @@ class MainActivity : AppCompatActivity() {
extras.putParcelable("product", null)
addProductIntent.putExtras(extras)
ContextCompat.startActivity(this, addProductIntent, extras)
}
}
}

View File

@ -0,0 +1,38 @@
package org.foxarmy.barcodescannerforemployees.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
class NavigatorActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var intent = Intent(this, LoginActivity::class.java)
if (isAuthenticated()) {
intent = Intent(this, MainActivity::class.java);
}
startActivity(intent);
finish();
}
fun isAuthenticated(): Boolean {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val sharedPreferences = EncryptedSharedPreferences.create(
// passing a file name to share a preferences
"sensitive",
masterKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val jwt = sharedPreferences.getString("token", "")
return jwt != ""
}
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:text="@string/register"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/registerButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/loginButton" app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintBottom_toTopOf="@+id/offlineButton" android:layout_marginBottom="64dp"/>
<Button
android:text="@string/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/loginButton"
app:layout_constraintStart_toEndOf="@+id/registerButton" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="2dp"
app:layout_constraintHorizontal_bias="0.1" app:layout_constraintBottom_toTopOf="@+id/offlineButton"
android:layout_marginBottom="64dp"/>
<Button
android:text="@string/offline"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/offlineButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="300dp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="50dp"
android:inputType="text"
android:ems="10"
android:id="@+id/usernameTextEdit"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:hint="@string/username" app:layout_constraintBottom_toTopOf="@+id/passwordTextEdit"
android:layout_marginBottom="16dp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="50dp"
android:inputType="textPassword"
android:ems="10"
android:id="@+id/passwordTextEdit"
android:hint="@string/password" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@+id/registerButton"
android:layout_marginBottom="24dp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text"
android:ems="10"
android:id="@+id/serverTextEdit"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/usernameTextEdit" android:layout_marginBottom="16dp"
android:hint="@string/server" android:text="bsfe.foxarmy.org"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,23 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.MainActivity">
tools:context=".activities.MainActivity"
android:fitsSystemWindows="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="@+id/storageLayout">
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
>
android:id="@+id/appBarLayout">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
@ -30,32 +33,42 @@
android:layout_height="wrap_content"
app:tabIndicatorColor="#FFF"
app:tabIndicatorHeight="3dp"
app:tabMode="fixed" />
app:tabMode="fixed"/>
</com.google.android.material.appbar.AppBarLayout>
<!-- <include layout="@layout/content_storage"/>-->
<androidx.viewpager.widget.ViewPager
android:id="@+id/tab_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
android:layout_width="0dp"
android:layout_height="777dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_gravity="bottom"
android:layout_marginTop="2dp"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="6dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="6dp"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_element_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_input_add"/>
app:srcCompat="@android:drawable/ic_input_add"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="16dp" android:layout_marginBottom="16dp"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true" app:srcCompat="@android:drawable/ic_menu_my_calendar"
android:id="@+id/expiryCalendarFab" android:layout_gravity="bottom|end"
tools:layout_editor_absoluteY="742dp" tools:layout_editor_absoluteX="337dp"
android:layout_marginBottom="84dp" android:layout_marginRight="16dp"/>
app:layout_constraintBottom_toTopOf="@+id/new_element_fab" android:layout_marginBottom="16dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- <include layout="@layout/content_storage"/>-->
<com.google.android.material.navigation.NavigationView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/nav_account"
android:title="@string/my_account" />
<item
android:id="@+id/nav_settings"
android:title="@string/settings" />
<item
android:id="@+id/nav_logout"
android:title="@string/my_groups" />
</menu>

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.foxarmy.barcodescannerforemployees.activities.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:title="@string/settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_delete" android:title="@string/delete_menu"/>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">СканнерШтрихкодовДляРаботников</string>
<string name="action_settings">Настройки</string>
<string name="settings">Настройки</string>
<string name="first_fragment_label">Добавить продукт</string>
<string name="second_fragment_label">Продукты</string>
<string name="scan_label">Сканировать</string>
@ -87,6 +87,16 @@
ошибка сканирования или введите данные вручную
</string>
<string name="abstract_product_request">Пожалуйста, отсканируйте штрихкод, чтобы добавить продукт</string>
<string name="no_barcode">No barcode present</string>
<string name="no_barcode_button">No barcode</string>
<string name="no_barcode">Нет штрихкода</string>
<string name="no_barcode_button">Без штрихкода</string>
<string name="register">Зарегистрироваться</string>
<string name="login">Войти</string>
<string name="offline">Оффлайн</string>
<string name="username">Имя пользователя</string>
<string name="password">Пароль</string>
<string name="username_already_exists">Имя пользователя занято</string>
<string name="wrong_password">Неправильный пароль</string>
<string name="server">Сервер</string>
<string name="my_account">Мой аккаунт</string>
<string name="my_groups">Мои группы</string>
</resources>

View File

@ -1,6 +1,6 @@
<resources>
<string name="app_name">BarcodeScannerForEmployees</string>
<string name="action_settings">Settings</string>
<string name="settings">Settings</string>
<string name="first_fragment_label">Add new product</string>
<string name="second_fragment_label">Products</string>
<string name="netWeight">Net weight</string>
@ -87,4 +87,14 @@
<string name="abstract_product_request">Please, scan a barcode in order to add product</string>
<string name="no_barcode">No barcode present</string>
<string name="no_barcode_button">No barcode</string>
<string name="register">Register</string>
<string name="login">Login</string>
<string name="offline">Offline</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="username_already_exists">Such account already exists</string>
<string name="wrong_password">Wrong password</string>
<string name="server">Server</string>
<string name="my_account">My account</string>
<string name="my_groups">My groups</string>
</resources>

View File

@ -17,15 +17,19 @@ gridlayout = "1.0.0"
activity = "1.9.2"
legacySupportV4 = "1.0.0"
fragment = "1.8.4"
okhttp = "4.10.0"
playServicesCodeScanner = "16.1.0"
securityCrypto = "1.0.0"
volley = "1.2.1"
zxingAndroidEmbedded = "4.3.0"
material3Android = "1.3.0"
[libraries]
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraView" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraView" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraView" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@ -40,9 +44,11 @@ androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", vers
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "playServicesCodeScanner" }
volley = { module = "com.android.volley:volley", version.ref = "volley" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }