Skip to content

Commit

Permalink
Adding API on cmd.FileReader for reading a file as ArrayBuffer
Browse files Browse the repository at this point in the history
This commit adds initial implementation of support for
dom.FileReader.readAsArrayBuffer as public API of the cmd.FileReader
object exposing a function that reads the file as a Vector[Byte] using
the typdarray api.

Updated goodies.md with new API
  • Loading branch information
KristianAN authored and davesmith00000 committed Jan 23, 2024
1 parent 7030f1c commit b23bb9a
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 14 deletions.
2 changes: 1 addition & 1 deletion docs/02-guides/goodies.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Tyrian comes with a number of handy functions built-in that you can make use of
These nuggets of functionality are used as commands.

- `Dom` - A few methods such as `focus` and `blur` to manipulate the DOM. Inspired by the Elm [Browser.Dom](https://package.elm-lang.org/packages/elm/browser/latest/Browser.Dom) package.
- `FileReader` - Given the id of a file input field that has had a file selected, this Cmd will read either an image or text file to return an `HTMLImageElement` or `String` respectively.
- `FileReader` - Given the id of a file input field that has had a file selected, this Cmd will read either raw bytes, an image or text file to return a `Vector[Byte]` or an `HTMLImageElement` or `String` respectively.
- `Http` - Make HTTP requests that return their responses as a message.
- `ImageLoader` - Given a path, this cmd will load an image and return an `HTMLImageElement` for you to make use of.
- `LocalStorage` - Allows you to save and load to/from your browsers local storage.
Expand Down
32 changes: 31 additions & 1 deletion sandbox/src/main/scala/example/Sandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,27 @@ object Sandbox extends TyrianIOApp[Msg, Model]:
case Msg.FileImageRead(data) =>
(model.copy(loadedImage = Option(data)), Cmd.None)

// Bytes file
case Msg.SelectBytesFile =>
val cmd: Cmd[IO, Msg] = File.select("application/octet-stream")(f => Msg.ReadBytesFile(f))

(model, cmd)

case Msg.ReadBytesFile(file) =>
val cmd: Cmd[IO, Msg] =
FileReader.readBytes(file)((r: FileReader.Result[Vector[Byte]]) =>
r match {
case FileReader.Result.File(_, _, d) =>
Msg.FileBytesRead(d)
case _ => Msg.NoOp
}
)

(model, cmd)

case Msg.FileBytesRead(data) =>
(model.copy(loadedBytes = Option(data)), Cmd.None)

// Text file

case Msg.SelectTextFile =>
Expand Down Expand Up @@ -612,12 +633,16 @@ object Sandbox extends TyrianIOApp[Msg, Model]:
div(
button(onClick(Msg.SelectImageFile))("Select an image file"),
button(onClick(Msg.SelectTextFile))("Select a text file"),
button(onClick(Msg.SelectBytesFile))("Select a file as bytes"),
div(style := CSS.width("200px"))(
model.loadedImage
.map(data => img(src := data))
.toList ++
model.loadedText
.map(data => p(data))
.toList ++
model.loadedBytes
.map(data => p(s"Read ${data.length} bytes"))
.toList
)
)
Expand Down Expand Up @@ -730,6 +755,9 @@ enum Msg:
case SelectTextFile
case ReadTextFile(file: dom.File)
case FileTextRead(fileData: String)
case SelectBytesFile
case ReadBytesFile(file: dom.File)
case FileBytesRead(fileData: Vector[Byte])
case NoOp

enum Status:
Expand Down Expand Up @@ -780,7 +808,8 @@ final case class Model(
fruit: List[Fruit],
fruitInput: String,
loadedImage: Option[String],
loadedText: Option[String]
loadedText: Option[String],
loadedBytes: Option[Vector[Byte]]
)

final case class Fruit(name: String, available: Boolean)
Expand Down Expand Up @@ -835,6 +864,7 @@ object Model:
Nil,
"",
None,
None,
None
)

Expand Down
56 changes: 44 additions & 12 deletions tyrian/js/src/main/scala/tyrian/cmds/FileReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import tyrian.Cmd

import scala.concurrent.Promise
import scala.scalajs.js
import scala.scalajs.js.typedarray

/** Given the id of a file input field that has had a file selected, this Cmd will read either an image or text file to
* return an `HTMLImageElement` or `String` respectively.
/** Given the id of a file input field that has had a file selected, this Cmd will read either raw bytes, an image or
* text file to return a `Vector[Byte]` or an `HTMLImageElement` or `String` respectively.
*/
object FileReader:

Expand All @@ -23,7 +24,7 @@ object FileReader:
try Result.File(n, p, d.asInstanceOf[String])
catch case _ => Result.Error("File is not a base64 string of image data")

readFromInputField(inputFieldId, false)(cast andThen resultToMessage)
readFromInputField(inputFieldId, ReadType.AsDataUrl)(cast andThen resultToMessage)

/** Reads an input file as base64 encoded image data */
def readImage[F[_]: Async, Msg](file: dom.File)(resultToMessage: Result[String] => Msg): Cmd[F, Msg] =
Expand All @@ -33,7 +34,7 @@ object FileReader:
try Result.File(n, p, d.asInstanceOf[String])
catch case _ => Result.Error("File is not a base64 string of image data")

readFile(file, false)(cast andThen resultToMessage)
readFile(file, ReadType.AsDataUrl)(cast andThen resultToMessage)

/** Reads an input file from an input field as plain text */
def readText[F[_]: Async, Msg](inputFieldId: String)(resultToMessage: Result[String] => Msg): Cmd[F, Msg] =
Expand All @@ -43,7 +44,7 @@ object FileReader:
try Result.File(n, p, d.asInstanceOf[String])
catch case _ => Result.Error("File is not text")

readFromInputField(inputFieldId, true)(cast andThen resultToMessage)
readFromInputField(inputFieldId, ReadType.AsText)(cast andThen resultToMessage)

/** Reads an input file as plain text */
def readText[F[_]: Async, Msg](file: dom.File)(resultToMessage: Result[String] => Msg): Cmd[F, Msg] =
Expand All @@ -53,16 +54,36 @@ object FileReader:
try Result.File(n, p, d.asInstanceOf[String])
catch case _ => Result.Error("File is not text")

readFile(file, true)(cast andThen resultToMessage)
readFile(file, ReadType.AsText)(cast andThen resultToMessage)

private def readFromInputField[F[_]: Async, Msg](fileInputFieldId: String, isText: Boolean)(
/** Reads an input file from an input field as bytes */
def readBytes[F[_]: Async, Msg](inputFieldId: String)(resultToMessage: Result[Vector[Byte]] => Msg): Cmd[F, Msg] =
val cast: Result[js.Any] => Result[Vector[Byte]] =
case Result.Error(msg) => Result.Error(msg)
case Result.File(n, p, d) =>
try Result.File(n, p, new typedarray.Int8Array(d.asInstanceOf[typedarray.ArrayBuffer]).toVector)
catch case _ => Result.Error("File is not bytes")

readFromInputField(inputFieldId, ReadType.AsArrayBuffer)(cast andThen resultToMessage)

/** Reads an input file as bytes */
def readBytes[F[_]: Async, Msg](file: dom.File)(resultToMessage: Result[Vector[Byte]] => Msg): Cmd[F, Msg] =
val cast: Result[js.Any] => Result[Vector[Byte]] =
case Result.Error(msg) => Result.Error(msg)
case Result.File(n, p, d) =>
try Result.File(n, p, new typedarray.Int8Array(d.asInstanceOf[typedarray.ArrayBuffer]).toVector)
catch case _ => Result.Error("File is not bytes")

readFile(file, ReadType.AsArrayBuffer)(cast andThen resultToMessage)

private def readFromInputField[F[_]: Async, Msg](fileInputFieldId: String, readType: ReadType)(
resultToMessage: Result[js.Any] => Msg
): Cmd[F, Msg] =
val files = document.getElementById(fileInputFieldId).asInstanceOf[html.Input].files
if files.length == 0 then Cmd.None
else readFile(files.item(0), isText)(resultToMessage)
else readFile(files.item(0), readType)(resultToMessage)

private def readFile[F[_]: Async, Msg](file: dom.File, isText: Boolean)(
private def readFile[F[_]: Async, Msg](file: dom.File, readAsType: ReadType)(
resultToMessage: Result[js.Any] => Msg
): Cmd[F, Msg] =
val task = Async[F].delay {
Expand All @@ -74,22 +95,33 @@ object FileReader:
p.success(
Result.File(
name = file.name,
path = e.target.asInstanceOf[js.Dynamic].result.asInstanceOf[String],
path = readAsType match
case ReadType.AsDataUrl => e.target.asInstanceOf[js.Dynamic].result.asInstanceOf[String]
case _ => ""
,
data = fileReader.result
)
),
false
)
fileReader.onerror = _ => p.success(Result.Error(s"Error reading from file"))

if isText then fileReader.readAsText(file)
else fileReader.readAsDataURL(file)
readAsType match
case ReadType.AsText => fileReader.readAsText(file)
case ReadType.AsArrayBuffer =>
fileReader.readAsArrayBuffer(file)
case ReadType.AsDataUrl => fileReader.readAsDataURL(file)

p.future
}

Cmd.Run(Async[F].fromFuture(task), resultToMessage)

private enum ReadType derives CanEqual:
case AsText
case AsArrayBuffer
case AsDataUrl

enum Result[A]:
case Error(message: String)
case File(name: String, path: String, data: A)

0 comments on commit b23bb9a

Please sign in to comment.