diff --git a/build.sbt b/build.sbt index 81e32aa..11ed192 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,7 @@ val sttp = "3.5.0" val anorm = "2.7.0" val scalaTestPlusPlay = "6.0.0-M6" val scalaTestPlusMockito = "3.2.15.0" -val reactAdmin = "4.14.3" +val reactAdmin = "4.14.4" val consoleDisabledOptions = Seq("-Xfatal-warnings", "-Ywarn-unused", "-Ywarn-unused-import") diff --git a/spra-play-server/src/main/resources/application.conf b/spra-play-server/src/main/resources/application.conf index 1fe710f..1a7b3da 100644 --- a/spra-play-server/src/main/resources/application.conf +++ b/spra-play-server/src/main/resources/application.conf @@ -30,6 +30,16 @@ dataExplorer { } referenceDisplayField = "email" } + + images { + tableName = "images" + primaryKeyField = "image_id" + nonEditableColumns = ["image_id", "created_at"] + canBeDeleted = false + createFilter { + requiredColumns = ["name", "data"] + } + } } } diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala new file mode 100644 index 0000000..5781e02 --- /dev/null +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala @@ -0,0 +1,8 @@ +package net.wiringbits.spra.admin.models + +trait FieldValue[T] extends Serializable { + val value: T +} + +case class StringValue(value: String) extends FieldValue[String] +case class ByteArrayValue(value: Array[Byte]) extends FieldValue[Array[Byte]] diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala index 4319c31..aba603c 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala @@ -2,8 +2,10 @@ package net.wiringbits.spra.admin.repositories import net.wiringbits.spra.admin.config.{DataExplorerConfig, TableSettings} import net.wiringbits.spra.admin.executors.DatabaseExecutionContext +import net.wiringbits.spra.admin.models.{ByteArrayValue, StringValue} import net.wiringbits.spra.admin.repositories.daos.DatabaseTablesDAO import net.wiringbits.spra.admin.repositories.models.{DatabaseTable, ForeignKey, TableColumn, TableData} +import net.wiringbits.spra.admin.utils.StringParse import net.wiringbits.spra.admin.utils.models.QueryParameters import play.api.db.Database @@ -75,7 +77,10 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit val fieldsAndValues = body.map { case (key, value) => val field = columns.find(_.name == key).getOrElse(throw new RuntimeException(s"Invalid property in body request: $key")) - (field, value) + if (field.`type` == "bytea") + val byteaValue = StringParse.stringToByteArray(value) + (field, ByteArrayValue(byteaValue)) + else (field, StringValue(value)) } DatabaseTablesDAO.create( tableName = tableName, @@ -100,7 +105,10 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit val fieldsAndValues = bodyWithoutNonEditableColumns.map { case (key, value) => val field = columns.find(_.name == key).getOrElse(throw new RuntimeException(s"Invalid property in body request: $key")) - (field, value) + if (field.`type` == "bytea") + val byteaValue = StringParse.stringToByteArray(value) + (field, ByteArrayValue(byteaValue)) + else (field, StringValue(value)) } val primaryKeyType = settings.primaryKeyDataType DatabaseTablesDAO.update( diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 2aed493..ab0e804 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -2,6 +2,7 @@ package net.wiringbits.spra.admin.repositories.daos import anorm.{SqlParser, SqlStringInterpolation} import net.wiringbits.spra.admin.config.{CustomDataType, PrimaryKeyDataType, TableSettings} +import net.wiringbits.spra.admin.models.{ByteArrayValue, FieldValue, StringValue} import net.wiringbits.spra.admin.repositories.models.* import net.wiringbits.spra.admin.utils.models.{FilterParameter, QueryParameters} import net.wiringbits.spra.admin.utils.{QueryBuilder, StringRegex} @@ -10,7 +11,7 @@ import java.sql.{Connection, Date, PreparedStatement, ResultSet} import java.time.LocalDate import java.util.UUID import scala.collection.mutable.ListBuffer -import scala.util.Try +import scala.util.{Failure, Success, Try} object DatabaseTablesDAO { @@ -73,6 +74,37 @@ object DatabaseTablesDAO { """.as(foreignKeyParser.*) } + private def columnTypeIsDouble(columnType: String): Boolean = { + // 'contains' is used because PostgreSQL types may include additional details like precision or scale + // https://www.postgresql.org/docs/8.1/datatype.html + List("float", "decimal").exists(columnType.contains) + } + + private def columnTypeIsInt(columnType: String): Boolean = { + List("int", "serial").exists(columnType.contains) + } + + private def isUUID(value: String, columnType: String): Boolean = { + Try(UUID.fromString(value)) match { + case Success(_) => columnType == "uuid" + case Failure(_) => false + } + } + + private def isInt(value: String, columnType: String): Boolean = { + value.toIntOption.isDefined && columnTypeIsInt(columnType) + } + + private def isDecimal(value: String, columnType: String): Boolean = { + value.toDoubleOption.isDefined && columnTypeIsDouble(columnType) + } + + private def isNumberOrUUID(value: String, columnType: String): Boolean = { + isInt(value, columnType) || + isDecimal(value, columnType) || + isUUID(value, columnType) + } + def getTableData( settings: TableSettings, columns: List[TableColumn], @@ -88,12 +120,15 @@ object DatabaseTablesDAO { val conditionsSql = queryParameters.filters .map { case FilterParameter(filterField, filterValue) => + val columnType = columns.find(_.name == filterField) match { + case Some(column) => column.`type` + case None => throw Exception(s"Column with name '$filterField' not found.") + } filterValue match { - case dateRegex(_, _, _) => + case dateRegex(_, _, _) if columnType == "date" => s"DATE($filterField) = ?" - case _ => - if (filterValue.toIntOption.isDefined || filterValue.toDoubleOption.isDefined) + if (isNumberOrUUID(filterValue, columnType)) s"$filterField = ?" else s"$filterField LIKE ?" @@ -111,20 +146,25 @@ object DatabaseTablesDAO { val preparedStatement = conn.prepareStatement(sql) queryParameters.filters.zipWithIndex - .foreach { case (FilterParameter(_, filterValue), index) => + .foreach { case (FilterParameter(filterField, filterValue), index) => // We have to increment index by 1 because SQL parameterIndex starts in 1 val sqlIndex = index + 1 - + val columnType = columns.find(_.name == filterField) match { + case Some(column) => column.`type` + case None => throw Exception(s"Column with name '$filterField' not found.") + } filterValue match { - case dateRegex(year, month, day) => + case dateRegex(year, month, day) if columnType == "date" => val parsedDate = LocalDate.of(year.toInt, month.toInt, day.toInt) preparedStatement.setDate(sqlIndex, Date.valueOf(parsedDate)) case _ => - if (filterValue.toIntOption.isDefined) + if (isInt(filterValue, columnType)) preparedStatement.setInt(sqlIndex, filterValue.toInt) - else if (filterValue.toDoubleOption.isDefined) + else if (isDecimal(filterValue, columnType)) preparedStatement.setDouble(sqlIndex, filterValue.toDouble) + else if (isUUID(filterValue, columnType)) + preparedStatement.setObject(sqlIndex, UUID.fromString(filterValue)) else preparedStatement.setString(sqlIndex, s"%$filterValue%") } @@ -230,7 +270,7 @@ object DatabaseTablesDAO { } def create( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID )(implicit @@ -250,7 +290,7 @@ object DatabaseTablesDAO { // Postgres: INSERT INTO test_serial (id) VALUES(DEFAULT); MySQL: INSERT INTO table (id) VALUES(NULL) for (j <- i + 1 to fieldsAndValues.size + i) { - val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1)) + val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1)).value preparedStatement.setObject(j, value) } val result = preparedStatement.executeQuery() @@ -260,7 +300,7 @@ object DatabaseTablesDAO { def update( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyValue: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID @@ -268,9 +308,9 @@ object DatabaseTablesDAO { val sql = QueryBuilder.update(tableName, fieldsAndValues, primaryKeyField) val preparedStatement = conn.prepareStatement(sql) - val notNullData = fieldsAndValues.filterNot { case (_, value) => value == "null" } + val notNullData = fieldsAndValues.filterNot { case (_, value) => value.value == "null" } notNullData.zipWithIndex.foreach { case ((_, value), i) => - preparedStatement.setObject(i + 1, value) + preparedStatement.setObject(i + 1, value.value) } // where ... = ? setPreparedStatementKey(preparedStatement, primaryKeyValue, primaryKeyType, notNullData.size + 1) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala index 8193eba..e7e3e40 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala @@ -1,6 +1,7 @@ package net.wiringbits.spra.admin.utils import net.wiringbits.spra.admin.config.PrimaryKeyDataType +import net.wiringbits.spra.admin.models.FieldValue import net.wiringbits.spra.admin.repositories.models.TableColumn import scala.collection.mutable @@ -8,7 +9,7 @@ import scala.collection.mutable object QueryBuilder { def create( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID ): String = { @@ -33,10 +34,10 @@ object QueryBuilder { |""".stripMargin } - def update(tableName: String, body: Map[TableColumn, String], primaryKeyField: String): String = { + def update(tableName: String, body: Map[TableColumn, FieldValue[_]], primaryKeyField: String): String = { val updateStatement = new mutable.StringBuilder("SET") for ((tableField, value) <- body) { - val resultStatement = if (value == "null") "NULL" else s"?::${tableField.`type`}" + val resultStatement = if (value.value == "null") "NULL" else s"?::${tableField.`type`}" val statement = s" ${tableField.name} = $resultStatement," updateStatement.append(statement) } diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala new file mode 100644 index 0000000..7d234d4 --- /dev/null +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala @@ -0,0 +1,14 @@ +package net.wiringbits.spra.admin.utils + +import scala.util.{Failure, Success, Try} + +object StringParse { + + def stringToByteArray(value: String): Array[Byte] = { + // Removes whitespace characters (\\s) and brackets ([, ]) to prepare the string for byte array conversion + Try(value.replaceAll("[\\[\\]\\s]", "").split(",").map(_.toByte)) match + case Success(value) => value + case Failure(_) => Array.emptyByteArray + } + +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala index d80196b..4cfe8cb 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala @@ -3,7 +3,7 @@ package net.wiringbits.spra.ui.web import net.wiringbits.spra.api.models.AdminGetTables import net.wiringbits.spra.ui.web.components.{CreateGuesser, EditGuesser, ListGuesser} import net.wiringbits.spra.ui.web.facades.reactadmin.{Admin, Resource} -import net.wiringbits.spra.ui.web.facades.simpleRestProvider +import net.wiringbits.spra.ui.web.facades.createDataProvider import net.wiringbits.spra.ui.web.models.DataExplorerSettings import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global import slinky.core.facade.{Hooks, ReactElement} @@ -52,7 +52,7 @@ object AdminView { } div()( - Admin(simpleRestProvider(tablesUrl))(buildResources), + Admin(createDataProvider(tablesUrl))(buildResources), error.map(h1(_)) ) } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala index d348793..4aa6712 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala @@ -29,7 +29,7 @@ object CreateGuesser { case ColumnType.Email => TextInput(source = field.name, isRequired = isRequired, validate = required) case ColumnType.Image => - ImageField(source = field.name, isRequired = isRequired, validate = required) + ImageInput(source = field.name, isRequired = isRequired, validate = required)(ImageField(source = "src")) case ColumnType.Number => NumberInput(source = field.name, isRequired = isRequired, validate = required) case ColumnType.Reference(reference, source) => diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala index 5b468f9..8e0a161 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala @@ -33,7 +33,7 @@ object EditGuesser { case ColumnType.Date => DateTimeInput(source = field.name, disabled = field.disabled) case ColumnType.Text => TextInput(source = field.name, disabled = field.disabled) case ColumnType.Email => TextInput(source = field.name, disabled = field.disabled) - case ColumnType.Image => ImageField(source = field.name) + case ColumnType.Image => ImageInput(source = field.name)(ImageField(source = "src")) case ColumnType.Number => NumberInput(source = field.name, disabled = field.disabled) case ColumnType.Reference(reference, source) => ReferenceInput( diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala index eacb477..64fe18d 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala @@ -20,7 +20,7 @@ object ListGuesser { val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props => val fields = ResponseGuesser.getTypesFromResponse(props.response) - def defaultField(reference: String, source: String)(children: ReactElement*): ReactElement = + def defaultField(reference: String, source: String)(children: ReactElement): ReactElement = ReferenceField(reference = reference, source = source)(children) val widgetFields: Seq[ReactElement] = fields.map { field => @@ -49,7 +49,14 @@ object ListGuesser { case ColumnType.Image => Fragment() case ColumnType.Number => NumberInput(source = field.name) case ColumnType.Reference(reference, source) => - defaultField(reference, field.name)(TextField(source = source)) + ReferenceInput( + source = field.name, + reference = reference + )( + SelectInput( + optionText = props.response.referenceDisplayField.getOrElse(source) + ) + ) } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala index 020648c..08a6ae6 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala @@ -1,6 +1,19 @@ package net.wiringbits.spra.ui.web.facades import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport @js.native trait DataProvider extends js.Object + +@js.native +@JSImport("ra-data-simple-rest", JSImport.Default) +// https://www.npmjs.com/package/ra-data-simple-rest +def simpleRestProvider(url: String): DataProvider = js.native + +@js.native +@JSImport("react-admin", "withLifecycleCallbacks") +// https://marmelab.com/react-admin/withLifecycleCallbacks.html +object WithLifecycleCallbacks extends js.Object { + def apply(dataProvider: DataProvider, callbacks: js.Array[js.Object]): DataProvider = js.native +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala index f6c484e..46f61ff 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala @@ -1,10 +1,33 @@ package net.wiringbits.spra.ui.web import scala.scalajs.js -import scala.scalajs.js.annotation.JSImport +import net.wiringbits.spra.ui.web.utils.Images.* +import org.scalajs.dom.File package object facades { - @js.native - @JSImport("ra-data-simple-rest", JSImport.Default) - def simpleRestProvider(url: String): DataProvider = js.native + + def createDataProvider(url: String): DataProvider = { + val baseDataProvider = simpleRestProvider(url) + WithLifecycleCallbacks( + baseDataProvider, + js.Array( + js.Dynamic.literal( + resource = "images", + afterRead = (record: js.Dynamic, dataProvider: js.Any) => { + val hexImage = record.data.asInstanceOf[String] + val urlImage = convertHexToImage(hexImage) + record.updateDynamic("data")(urlImage) + record + }, + beforeSave = (data: js.Dynamic, dataProvider: js.Any) => { + val rawFile = data.data.rawFile.asInstanceOf[File] + convertImageToByteArray(rawFile).`then` { value => + data.updateDynamic("data")(value.asInstanceOf[js.Any]) + data + } + } + ) + ) + ) + } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala index 0330951..fa440e4 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala @@ -8,20 +8,20 @@ import scala.scalajs.js.| object ImageField extends ExternalComponent { case class Props( source: String, + title: String, + sortable: Boolean = false, disabled: Boolean = false, - sx: js.Dynamic = js.Dynamic.literal(), - isRequired: Boolean = false, - validate: js.UndefOr[js.Any] = js.undefined + sx: js.Dynamic = js.Dynamic.literal() ) def apply( source: String, + title: String = "title", + sortable: Boolean = false, disabled: Boolean = false, - sx: js.Dynamic = js.Dynamic.literal(), - isRequired: Boolean = false, - validate: js.UndefOr[js.Any] = js.undefined + sx: js.Dynamic = js.Dynamic.literal() ): BuildingComponent[_, _] = { - super.apply(Props(source, disabled, sx, isRequired, validate)) + super.apply(Props(source, title, sortable, disabled, sx)) } override val component: String | js.Object = ReactAdmin.ImageField diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala new file mode 100644 index 0000000..5146380 --- /dev/null +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala @@ -0,0 +1,30 @@ +package net.wiringbits.spra.ui.web.facades.reactadmin + +import slinky.core.{BuildingComponent, ExternalComponent} + +import scala.scalajs.js +import scala.scalajs.js.| + +// https://marmelab.com/react-admin/ImageInput.html +object ImageInput extends ExternalComponent { + case class Props( + source: String, + disabled: Boolean = false, + sx: js.Dynamic = js.Dynamic.literal(), + isRequired: Boolean = false, + validate: js.UndefOr[js.Any] = js.undefined + ) + + def apply( + source: String, + disabled: Boolean = false, + sx: js.Dynamic = js.Dynamic.literal(), + isRequired: Boolean = false, + validate: js.UndefOr[js.Any] = js.undefined, + onDrop: js.UndefOr[js.Function2[js.Array[js.Any], js.Function1[js.Array[js.Object], Unit], Unit]] = js.undefined + ): BuildingComponent[_, _] = { + super.apply(Props(source, disabled, sx, isRequired, validate)) + } + + override val component: String | js.Object = ReactAdmin.ImageInput +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala index 6d34e3f..ead6b49 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala @@ -11,10 +11,10 @@ package object reactadmin { def required(): js.Any = js.native - val Admin, Resource, EditGuesser, ListGuesser, TextInput, ImageField, NumberInput, DateTimeInput, ReferenceInput, - SelectInput, Button, DeleteButton, SaveButton, TopToolbar, Toolbar, Edit, SimpleForm, DateField, TextField, - EmailField, NumberField, ReferenceField, DateInput, FilterButton, ExportButton, List, Datagrid, Create, - CreateButton: js.Object = + val Admin, Resource, EditGuesser, ListGuesser, TextInput, ImageField, ImageInput, NumberInput, DateTimeInput, + ReferenceInput, SelectInput, Button, DeleteButton, SaveButton, TopToolbar, Toolbar, Edit, SimpleForm, DateField, + TextField, EmailField, NumberField, ReferenceField, DateInput, FilterButton, ExportButton, List, Datagrid, + Create, CreateButton: js.Object = js.native } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala new file mode 100644 index 0000000..87efd81 --- /dev/null +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala @@ -0,0 +1,42 @@ +package net.wiringbits.spra.ui.web.utils + +import org.scalajs.dom +import org.scalajs.dom.{Blob, File} +import scala.util.{Failure, Success, Try} +import scala.scalajs.js.Promise +import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array, Uint8Array} +import scala.scalajs.js + +object Images { + + def convertImageToByteArray(image: dom.File): js.Promise[String] = { + new js.Promise[String]((resolve, reject) => { + val reader = new dom.FileReader() + reader.onload = { (e: dom.Event) => + val arrayBuffer = reader.result.asInstanceOf[ArrayBuffer] + val byteArray = new Int8Array(arrayBuffer).toArray + resolve(byteArray.mkString("[", ", ", "]")) + } + reader.onerror = { (e: dom.Event) => + reject(new js.Error("Failed to read file")) + } + reader.readAsArrayBuffer(image) + }) + } + + def convertHexToImage(imageHex: String): String = { + // Remove the "0x" prefix from the hex string, as it's not part of the actual image data + val hex = imageHex.tail.tail + val imageBinary: Array[Byte] = + if ((hex.length % 2) == 1) + Array.empty + else + Try(hex.grouped(2).map { hex => Integer.parseInt(hex, 16).toByte }.toArray) match { + case Success(value) => value + case Failure(_) => Array.empty + } + val byteArray = Uint8Array(js.Array(imageBinary.map(_.toShort): _*)) + dom.URL.createObjectURL(dom.Blob(js.Array(byteArray.buffer))) + } + +}