From 2cc7a3a2c3569495072480ad400949ef4edc4f84 Mon Sep 17 00:00:00 2001 From: Omni Devel Date: Sun, 21 Jun 2026 19:09:08 +0300 Subject: [PATCH] feat: added templates --- mobile/build.gradle.kts | 2 + mobile/src/main/AndroidManifest.xml | 17 ++ .../omni_devel/cards/ChooseColorActivity.kt | 118 ++++++++++++++ .../main/java/ru/omni_devel/cards/Config.kt | 3 + .../ru/omni_devel/cards/EditCardActivity.kt | 146 ++++++------------ .../omni_devel/cards/FillTemplateActivity.kt | 129 ++++++++++++++++ .../cards/SelectTemplateActivity.kt | 128 +++++++++++++++ .../main/java/ru/omni_devel/cards/Template.kt | 77 +++++++++ mobile/src/main/res/values-ru/strings.xml | 3 + mobile/src/main/res/values/strings.xml | 6 + 10 files changed, 526 insertions(+), 103 deletions(-) create mode 100644 mobile/src/main/java/ru/omni_devel/cards/ChooseColorActivity.kt create mode 100644 mobile/src/main/java/ru/omni_devel/cards/Config.kt create mode 100644 mobile/src/main/java/ru/omni_devel/cards/FillTemplateActivity.kt create mode 100644 mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt create mode 100644 mobile/src/main/java/ru/omni_devel/cards/Template.kt diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index a60c4d9..7b3ffe8 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -78,4 +78,6 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation(libs.compose.colorpicker) implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6") + implementation("io.ktor:ktor-client-core:3.5.0") + implementation("io.ktor:ktor-client-cio:3.5.0") } \ No newline at end of file diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 7f036b9..b6ec9f4 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + + + + + EditColorPage(innerPadding, currentColor) + } + } + } + } +} + +@Composable +fun EditColorPage(innerPadding: PaddingValues, currentColor: String?) { + val activityContext = LocalActivity.current + val configuration = LocalConfiguration.current + + val screenHeight = configuration.screenHeightDp.dp + val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT + + var color by rememberSaveable { mutableStateOf(currentColor) } + + val colorPickerController = rememberColorPickerController() + + Page( + stringResource(R.string.choosing_color), + innerPadding + ) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + var colorPickerModifier = Modifier + .padding(10.dp) + + if (isPortrait) { + colorPickerModifier = colorPickerModifier + .fillMaxWidth() + .height(450.dp) + } else { + val size = screenHeight * 0.5f + + colorPickerModifier = colorPickerModifier + .height(size) + .width(size) + } + + HsvColorPicker( + modifier = colorPickerModifier, + initialColor = color?.let { Color(it.toColorInt()) }, + controller = colorPickerController, + onColorChanged = { colorEnvelope: ColorEnvelope -> + color = "#${colorEnvelope.hexCode.substring(2)}" + } + ) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + val resultIntent = Intent() + + resultIntent.putExtra("color", color) + + activityContext!!.setResult(Activity.RESULT_OK, resultIntent) + activityContext.finish() + } + ) { + Text(stringResource(R.string.do_save)) + } + } + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/Config.kt b/mobile/src/main/java/ru/omni_devel/cards/Config.kt new file mode 100644 index 0000000..40264e1 --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/Config.kt @@ -0,0 +1,3 @@ +package ru.omni_devel.cards + +const val TEMPLATES_URL = "https://gitea.omni-devel.ru/omni/OmniCardsRepo/raw/branch/main/templates.json" diff --git a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt index 7af69c1..b39d438 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt @@ -77,85 +77,6 @@ class EditCardActivity : ComponentActivity() { } } -class SelectColorActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - - val currentColor = intent.getStringExtra("currentColor") - - setContent { - OmniCardsTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - EditColorPage(innerPadding, currentColor) - } - } - } - } -} - -@Composable -fun EditColorPage(innerPadding: PaddingValues, currentColor: String?) { - val activityContext = LocalActivity.current - val configuration = LocalConfiguration.current - - val screenHeight = configuration.screenHeightDp.dp - val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT - - var color by rememberSaveable { mutableStateOf(currentColor) } - - val colorPickerController = rememberColorPickerController() - - Page( - stringResource(R.string.choosing_color), - innerPadding - ) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - var colorPickerModifier = Modifier - .padding(10.dp) - - if (isPortrait) { - colorPickerModifier = colorPickerModifier - .fillMaxWidth() - .height(450.dp) - } else { - val size = screenHeight * 0.5f - - colorPickerModifier = colorPickerModifier - .height(size) - .width(size) - } - - HsvColorPicker( - modifier = colorPickerModifier, - initialColor = color?.let { Color(it.toColorInt()) }, - controller = colorPickerController, - onColorChanged = { colorEnvelope: ColorEnvelope -> - color = "#${colorEnvelope.hexCode.substring(2)}" - } - ) - - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - val resultIntent = Intent() - - resultIntent.putExtra("color", color) - - activityContext!!.setResult(Activity.RESULT_OK, resultIntent) - activityContext.finish() - } - ) { - Text(stringResource(R.string.do_save)) - } - } - } -} - @Composable fun CardEditable( cardName: String, @@ -184,7 +105,7 @@ fun CardEditable( result.formatName?.let { formatName -> try { onCodeTypeChange(BarcodeFormat.valueOf(formatName)) - } catch (e: IllegalArgumentException) {} + } catch (_: IllegalArgumentException) {} } } } @@ -200,35 +121,54 @@ fun CardEditable( } } + val selectTemplateLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val codeValue = result.data?.getStringExtra("codeValue") ?: "" + + onCodeValueChange(codeValue) + } + } + OutlinedTextField( value = cardName, onValueChange = onCardNameChange, modifier = fieldModifier, label = { Text(stringResource(R.string.card_name)) } ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = codeValue, - onValueChange = onCodeValueChange, - modifier = Modifier.weight(1f), - label = { Text(stringResource(R.string.card_value)) } - ) - Button( - modifier = Modifier.align(Alignment.CenterVertically), - onClick = { - scanLauncher.launch( - ScanOptions().apply { - setPrompt(context.getString(R.string.scan_your_card)) - setBeepEnabled(true) - setOrientationLocked(false) - } - ) - } - ) { - Text(stringResource(R.string.do_scan_card)) + + OutlinedTextField( + value = codeValue, + onValueChange = onCodeValueChange, + modifier = fieldModifier, + label = { Text(stringResource(R.string.card_value)) } + ) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + scanLauncher.launch( + ScanOptions().apply { + setPrompt(context.getString(R.string.scan_your_card)) + setBeepEnabled(true) + setOrientationLocked(false) + } + ) } + ) { + Text(stringResource(R.string.do_scan_card)) + } + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + val intent = Intent(context, SelectTemplateActivity::class.java) + + selectTemplateLauncher.launch(intent) + } + ) { + Text(stringResource(R.string.card_from_template)) } Box( @@ -272,7 +212,7 @@ fun CardEditable( Button( enabled = isColorEnabled, onClick = { - val intent = Intent(context, SelectColorActivity::class.java) + val intent = Intent(context, ChooseColorActivity::class.java) intent.putExtra("currentColor", currentColor) diff --git a/mobile/src/main/java/ru/omni_devel/cards/FillTemplateActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/FillTemplateActivity.kt new file mode 100644 index 0000000..a575a4f --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/FillTemplateActivity.kt @@ -0,0 +1,129 @@ +package ru.omni_devel.cards + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json +import ru.omni_devel.cards.ui.theme.OmniCardsTheme + +class FillTemplateActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val templateJson = intent.getStringExtra("templateJson") ?: "" + val template = Template.parseTemplateFromJson(templateJson) + + setContent { + OmniCardsTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + FillTemplatePage(template, innerPadding) + } + } + } + } +} + +@Composable +fun FillTemplatePage(template: Template, innerPadding: PaddingValues) { + val activityContext = LocalActivity.current + val context = LocalContext.current + + var filledFields by rememberSaveable { mutableStateOf(template.fields.associate { field -> + field.id to (field.default ?: "") + }) } + + Page( + template.name, + innerPadding + ) { + val fieldModifier = Modifier.fillMaxWidth() + + for (field in template.fields) { + OutlinedTextField( + value = filledFields[field.id]!!, + onValueChange = { newValue -> + filledFields = filledFields.toMutableMap().apply { + this[field.id] = newValue + } + }, + modifier = fieldModifier, + label = { Text((if (field.maybeEmpty) "" else "* ") + field.name) } + ) + } + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + var isSuccess = true + + for (field in template.fields) { + if (filledFields[field.id]!!.isEmpty() && !field.maybeEmpty) { + isSuccess = false + } + } + + if (!isSuccess) { + Toast.makeText(context, context.getString(R.string.error_in_data), Toast.LENGTH_LONG).show() + + return@Button + } + + var result = template.template + + for ((id, value) in filledFields) { + result = result.replace("$$id$", value) + } + + val resultIntent = Intent() + + resultIntent.putExtra("codeValue", result) + + activityContext!!.setResult(Activity.RESULT_OK, resultIntent) + activityContext.finish() + } + ) { + Text(stringResource(R.string.do_save)) + } + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt new file mode 100644 index 0000000..d3edd45 --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt @@ -0,0 +1,128 @@ +package ru.omni_devel.cards + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json +import ru.omni_devel.cards.ui.theme.OmniCardsTheme + +class SelectTemplateActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + OmniCardsTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + SelectTemplatePage(innerPadding) + } + } + } + } +} + +@Composable +fun SelectTemplatePage(innerPadding: PaddingValues) { + val activityContext = LocalActivity.current + val context = LocalContext.current + + var templates by remember { mutableStateOf?>(null) } + var loadingError by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + try { + val client = HttpClient(CIO) + + val response: HttpResponse = client.get(TEMPLATES_URL) + + templates = Template.parseTemplatesFromJson(response.bodyAsText()) + } catch (e: Exception) { + loadingError = e.toString() + } + } + + val fillTemplateLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val codeValue = result.data?.getStringExtra("codeValue") + + val resultIntent = Intent() + + resultIntent.putExtra("codeValue", codeValue) + + activityContext!!.setResult(Activity.RESULT_OK, resultIntent) + activityContext.finish() + } + } + + Page( + stringResource(R.string.choosing_template), + innerPadding + ) { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (loadingError != null) { + Text(loadingError!!) + } else if (templates == null) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + color = MaterialTheme.colorScheme.primary + ) + } else { + for (template in templates) { + Button( + modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.primaryContainer), + onClick = { + val intent = Intent(context, FillTemplateActivity::class.java) + + intent.putExtra("templateJson", Json.encodeToString(template)) + + fillTemplateLauncher.launch(intent) + } + ) { + Text(template.name) + } + } + } + } + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/Template.kt b/mobile/src/main/java/ru/omni_devel/cards/Template.kt new file mode 100644 index 0000000..68f326c --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/Template.kt @@ -0,0 +1,77 @@ +package ru.omni_devel.cards + +import kotlinx.serialization.Serializable +import org.json.JSONArray +import org.json.JSONObject + +@Serializable +class Field( + val id: String, + val name: String, + val default: String? = null, + val maybeEmpty: Boolean = false +) + +@Serializable +class Template( + val name: String, + val fields: List, + val template: String +) { + companion object { + fun parseTemplateFromJson(json: String): Template { + val obj = JSONObject(json) + + val fieldsArray = obj.getJSONArray("fields") + val fields = mutableListOf() + + for (fi in 0 until fieldsArray.length()) { + val fieldObj = fieldsArray.getJSONObject(fi) + + fields.add(Field( + id = fieldObj.getString("id"), + name = fieldObj.getString("name"), + default = fieldObj.optString("default"), + maybeEmpty = fieldObj.optBoolean("maybeEmpty") + )) + } + + return Template( + name = obj.getString("name"), + fields = fields, + template = obj.getString("template") + ) + } + + fun parseTemplatesFromJson(json: String): List