Browse Source

gse

development
Rohan Sircar 3 months ago
parent
commit
81765c6dcd
36 changed files with 1201 additions and 625 deletions
  1. +1
    -0
      .gitignore
  2. +6
    -3
      build.sbt
  3. +1
    -1
      project/build.properties
  4. +85
    -304
      src/main/resources/static/css/main.css
  5. +3
    -1
      src/main/scala/nova/monadic_sfx/Main.scala
  6. +257
    -131
      src/main/scala/nova/monadic_sfx/MainApp.scala
  7. +5
    -5
      src/main/scala/nova/monadic_sfx/MainModule.scala
  8. +2
    -0
      src/main/scala/nova/monadic_sfx/Types.scala
  9. +8
    -5
      src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
  10. +10
    -1
      src/main/scala/nova/monadic_sfx/implicits/package.scala
  11. +71
    -0
      src/main/scala/nova/monadic_sfx/ui/FXComponent.scala
  12. +42
    -24
      src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala
  13. +34
    -15
      src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala
  14. +10
    -13
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala
  15. +23
    -12
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala
  16. +33
    -12
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala
  17. +1
    -1
      src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala
  18. +37
    -0
      src/main/scala/nova/monadic_sfx/util/BoundedStack.scala
  19. +18
    -0
      src/main/scala/nova/monadic_sfx/util/CssPath.scala
  20. +49
    -1
      src/main/scala/nova/monadic_sfx/util/History.scala
  21. +31
    -0
      src/main/scala/nova/monadic_sfx/util/MediaPlayerResource.scala
  22. +47
    -0
      src/main/scala/nova/monadic_sfx/util/WebSocket.scala
  23. +3
    -0
      src/main/scala/nova/monadic_sfx/util/controls/EmbeddedMediaPlayer.scala
  24. +4
    -8
      src/main/scala/nova/monadic_sfx/util/controls/JFXButton.scala
  25. +22
    -0
      src/main/scala/nova/monadic_sfx/util/controls/JFXDialog.scala
  26. +21
    -2
      src/main/scala/nova/monadic_sfx/util/controls/JFXListCell.scala
  27. +2
    -10
      src/main/scala/nova/monadic_sfx/util/controls/JFXListView.scala
  28. +37
    -0
      src/main/scala/nova/monadic_sfx/util/controls/JFXRippler.scala
  29. +15
    -0
      src/main/scala/nova/monadic_sfx/util/controls/VideoView.scala
  30. +1
    -11
      src/main/scala/nova/monadic_sfx/util/reactive/store/Middlewares.scala
  31. +14
    -65
      src/main/scala/nova/monadic_sfx/util/reactive/store/Store.scala
  32. +32
    -0
      src/main/scala/nova/monadic_sfx/util/test.txt
  33. +107
    -0
      src/test/scala/HistoryTest.scala
  34. +83
    -0
      src/test/scala/ProSubjectTest.scala
  35. +61
    -0
      src/test/scala/WebSocketTest.scala
  36. +25
    -0
      src/test/scala/WebSocketTestServer.py

+ 1
- 0
.gitignore View File

@ -24,3 +24,4 @@ metals.sbt
/project/project
.bsp
.attach_pid**

+ 6
- 3
build.sbt View File

@ -8,7 +8,7 @@ version := "14-R19"
scalaVersion := "2.13.4"
// Add dependency on ScalaFX library
libraryDependencies += "org.scalafx" %% "scalafx" % "14-R19"
libraryDependencies += "org.scalafx" %% "scalafx" % "15.0.1-R20"
resolvers += Resolver.sonatypeRepo("snapshots")
enablePlugins(JavaFxPlugin)
@ -26,7 +26,8 @@ libraryDependencies ++= Seq(
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
"com.softwaremill.quicklens" %% "quicklens" % "1.6.1",
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
"com.softwaremill.macwire" %% "util" % "2.3.7",
// "com.softwaremill.macwire" %% "util" % "2.3.7",
"com.softwaremill.common" %% "tagging" % "2.2.1",
"com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided",
"com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided",
"com.github.valskalla" %% "odin-monix" % "0.9.1",
@ -48,7 +49,9 @@ libraryDependencies ++= Seq(
"fr.brouillard.oss" % "cssfx" % "11.4.0",
"com.lihaoyi" %% "sourcecode" % "0.2.1",
"eu.timepit" %% "refined" % "0.9.19",
"org.scalatest" %% "scalatest" % "3.2.2" % "test"
"org.scalatest" %% "scalatest" % "3.2.2" % "test",
"uk.co.caprica" % "vlcj" % "4.7.0",
"uk.co.caprica" % "vlcj-javafx" % "1.0.2"
)
scalacOptions ++= Seq(

+ 1
- 1
project/build.properties View File

@ -1,2 +1,2 @@
sbt.version=1.4.3
sbt.version=1.4.5

+ 85
- 304
src/main/resources/static/css/main.css View File

@ -1,342 +1,123 @@
.root {
-fx-font-family: Roboto;
src: "/resources/roboto/Roboto-Regular.ttf";
-fx-font-family: Roboto;
src: "/resources/roboto/Roboto-Regular.ttf";
-fx-text-fill: white;
-fx-background-color: #14161d;
/* rgb(38,38,38) */
}
/* Burgers Demo */
.jfx-hamburger {
-fx-spacing: 5;
-fx-cursor: hand;
}
.jfx-hamburger StackPane {
-fx-pref-width: 40px;
-fx-pref-height: 7px;
-fx-background-color: #D63333;
-fx-background-radius: 5px;
}
/* Input Demo */
.text-field {
-fx-max-width: 300;
}
.jfx-text-field, .jfx-password-field {
-fx-background-color: WHITE;
-fx-font-weight: BOLD;
-fx-prompt-text-fill: #808080;
-fx-alignment: top-left;
-jfx-focus-color: #4059A9;
-jfx-unfocus-color: #4d4d4d;
-fx-max-width: 300;
}
.jfx-decorator {
-fx-decorator-color: RED;
}
.jfx-decorator .jfx-decorator-buttons-container {
-fx-background-color: -fx-decorator-color;
}
.jfx-decorator .resize-border {
-fx-border-color: -fx-decorator-color;
-fx-border-width: 0 4 4 4;
}
.jfx-text-area, .text-area {
-fx-font-weight: BOLD;
}
.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error {
-jfx-focus-color: #D34336;
-jfx-unfocus-color: #D34336;
}
.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
-fx-text-fill: #D34336;
-fx-font-size: 0.75em;
}
.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
-fx-text-fill: #D34336;
-fx-font-size: 1em;
}
/* Progress Bar Demo */
.progress-bar > .bar {
-fx-min-width: 500;
}
.jfx-progress-bar > .bar {
-fx-min-width: 500;
}
.jfx-progress-bar {
-fx-progress-color: #0F9D58;
-fx-stroke-width: 3;
.text-white {
-fx-text-fill: white;
}
/* Icons Demo */
.icon {
-fx-text-fill: #FE774D;
-fx-padding: 10;
-fx-cursor: hand;
.clear-list-view .scroll-bar:horizontal .track,
.clear-list-view .scroll-bar:vertical .track {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-background-radius: 0em;
-fx-border-radius: 2em;
}
.icons-rippler {
-jfx-rippler-fill: BLUE;
-jfx-mask-type: CIRCLE;
.clear-list-view .scroll-bar:horizontal .increment-button,
.clear-list-view .scroll-bar:horizontal .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 0 10 0;
}
.icons-rippler:hover {
-fx-cursor: hand;
.clear-list-view .scroll-bar:vertical .increment-button,
.clear-list-view .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 10 0 0;
}
.jfx-check-box {
-fx-font-weight: BOLD;
}
.custom-jfx-check-box {
-jfx-checked-color: RED;
-jfx-unchecked-color: BLACK;
.clear-list-view .scroll-bar .increment-arrow,
.clear-list-view .scroll-bar .decrement-arrow {
-fx-shape: " ";
-fx-padding: 0;
}
/* Button */
.button {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14px;
.clear-list-view .scroll-bar:horizontal .thumb,
.clear-list-view .scroll-bar:vertical .thumb {
-fx-background-color: derive(black, 90%);
-fx-background-insets: 2, 0, 0;
-fx-background-radius: 2em;
}
.button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(77, 102, 204);
-fx-pref-width: 200;
-fx-text-fill: WHITE;
.scroll-pane .scroll-bar:horizontal .track,
.scroll-pane .scroll-bar:vertical .track {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-background-radius: 0em;
-fx-border-radius: 2em;
}
/* The main scrollbar **track** CSS class */
.mylistview .scroll-bar:horizontal .track,
.mylistview .scroll-bar:vertical .track {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-background-radius: 0em;
-fx-border-radius: 2em;
.scroll-pane .scroll-bar:horizontal .increment-button,
.scroll-pane .scroll-bar:horizontal .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 0 10 0;
}
/* The increment and decrement button CSS class of scrollbar */
.mylistview .scroll-bar:horizontal .increment-button,
.mylistview .scroll-bar:horizontal .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 0 10 0;
.scroll-pane .scroll-bar:vertical .increment-button,
.scroll-pane .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 10 0 0;
}
/* The increment and decrement button CSS class of scrollbar */
.mylistview .scroll-bar:vertical .increment-button,
.mylistview .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 10 0 0;
.scroll-pane .scroll-bar .increment-arrow,
.scroll-pane .scroll-bar .decrement-arrow {
-fx-shape: " ";
-fx-padding: 0;
}
.mylistview .scroll-bar .increment-arrow,
.mylistview .scroll-bar .decrement-arrow {
-fx-shape: " ";
-fx-padding: 0;
.scroll-pane .scroll-bar:horizontal .thumb,
.scroll-pane .scroll-bar:vertical .thumb {
-fx-background-color: derive(black, 90%);
-fx-background-insets: 2, 0, 0;
-fx-background-radius: 2em;
}
/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
.mylistview .scroll-bar:horizontal .thumb,
.mylistview .scroll-bar:vertical .thumb {
-fx-background-color: derive(black, 90%);
-fx-background-insets: 2, 0, 0;
-fx-background-radius: 2em;
.scroll-pane {
-fx-background-color: transparent;
}
.jfx-list-cell-container {
-fx-alignment: center-left;
.scroll-pane > .viewport {
-fx-background-color: transparent;
}
.jfx-list-cell-container > .label {
-fx-text-fill: BLACK;
.clear-list-view {
-fx-background-color: rgba(28, 28, 28, 28, 0.4);
-fx-text-fill: white;
-fx-background-radius: 20px;
-fx-padding: 5px 10px;
}
.jfx-list-cell:odd:selected > .jfx-rippler > StackPane, .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
-fx-background-color: rgba(0, 0, 255, 0.2);
.clear-list-view .list-cell {
-fx-background-color: transparent;
-fx-text-fill: white;
}
.jfx-list-cell {
-fx-background-insets: 0.0;
-fx-text-fill: BLACK;
.clear-list-view .list-cell:selected {
-fx-background-color: rgba(0, 0, 0, 0.2);
-fx-text-fill: white;
-fx-background-radius: 20px;
}
.jfx-list-cell:odd, .jfx-list-cell:even {
-fx-background-color: WHITE;
.clear-list-view .list-cell:hover {
-fx-background-color: rgba(0, 0, 0, 0.1);
-fx-text-fill: white;
-fx-background-radius: 20px;
}
.jfx-list-cell:filled:hover {
-fx-text-fill: black;
.clear-list-view .list-cell:selected:hover {
-fx-background-color: rgba(0, 0, 0, 0.3);
-fx-text-fill: white;
-fx-background-radius: 20px;
}
.jfx-list-cell .jfx-rippler {
-jfx-rippler-fill: BLUE;
.clear-list-view:focused .list-cell:selected {
-fx-background-color: rgba(0, 0, 0, 0.3);
-fx-text-fill: white;
-fx-background-radius: 20px;
}
.jfx-list-view {
-fx-background-insets: 0;
-jfx-cell-horizontal-margin: 0.0;
-jfx-cell-vertical-margin: 5.0;
-jfx-vertical-gap: 10;
-jfx-expanded: false;
-fx-pref-width: 200;
}
.jfx-toggle-button {
-jfx-toggle-color: RED;
}
.jfx-tool-bar {
-fx-font-size: 20;
-fx-font-weight: BOLD;
-fx-background-color: #5264AE;
-fx-pref-width: 100%;
-fx-pref-height: 64px;
}
.jfx-tool-bar HBox {
-fx-alignment: center;
-fx-spacing: 25;
-fx-padding: 0 10;
}
.jfx-tool-bar Label {
-fx-text-fill: WHITE;
}
.jfx-popup-container {
-fx-background-color: WHITE;
}
.jfx-snackbar-content {
-fx-background-color: #323232;
-fx-padding: 5;
-fx-spacing: 5;
}
.jfx-snackbar-toast {
-fx-text-fill: WHITE;
}
.jfx-snackbar-action {
-fx-text-fill: #ff4081;
}
.jfx-list-cell-content-container {
-fx-alignment: center-left;
}
.jfx-list-cell-container .label {
-fx-text-fill: BLACK;
}
.combo-box-popup .list-view .jfx-list-cell:odd:selected .jfx-list-cell-container,
.combo-box-popup .list-view .jfx-list-cell:even:selected .jfx-list-cell-container {
-fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
}
.combo-box-popup .list-view .jfx-list-cell {
-fx-background-insets: 0.0;
-fx-text-fill: BLACK;
}
.combo-box-popup .list-view .jfx-list-cell:odd,
.combo-box-popup .list-view .jfx-list-cell:even {
-fx-background-color: WHITE;
}
.combo-box-popup .list-view .jfx-list-cell:filled:hover {
-fx-text-fill: black;
}
/*.combo-box .combo-box-button-container{
-fx-border-color:BLACK;-fx-border-width: 0 0 1 0;
}
.combo-box .combo-box-selected-value-container{
-fx-border-color:BLACK;
} */
/*
* TREE TABLE CSS
*/
.tree-table-view {
-fx-tree-table-color: rgba(255, 0, 0, 0.2);
-fx-tree-table-rippler-color: rgba(255, 0, 0, 0.4);
}
.tree-table-view .jfx-text-field{
-fx-background-color: transparent;
}
.animated-option-button {
-fx-background-color: #F1F1F1;
-fx-background-radius: 50px;
-fx-pref-height: 50px;
-fx-pref-width: 50px;
-fx-min-width: -fx-pref-width;
-fx-max-width: -fx-pref-width;
-fx-min-height: -fx-pref-height;
-fx-max-height: -fx-pref-height;
-jfx-button-type: RAISED;
}
.animated-option-button .jfx-rippler{
-jfx-rippler-fill: rgb(113, 118, 114);
}
.sub-icon{
-fx-fill: rgb(113, 118, 114);
}
.main-button {
-fx-pref-width: 60px;
-fx-background-color: #0F9D58;
-fx-background-radius: 60px;
-fx-pref-height: 60px;
-fx-min-width: -fx-pref-width;
-fx-max-width: -fx-pref-width;
-fx-min-height: -fx-pref-height;
-fx-max-height: -fx-pref-height;
-jfx-button-type: RAISED;
}
.main-button .jfx-rippler{
-jfx-rippler-fill: rgba(255,255,255, .87);
}
.main-icon{
-fx-fill: rgba(255,255,255, .87);
}
.animated-option-sub-button {
-fx-background-color: #43609C;
}
.animated-option-sub-button2 {
-fx-background-color: rgb(203, 104, 96);
}
.tree-table-view .menu-item:focused {
-fx-background-color: -fx-tree-table-color;
}
.tree-table-view .menu-item .label {
-fx-padding: 5 0 5 0;
}

+ 3
- 1
src/main/scala/nova/monadic_sfx/Main.scala View File

@ -9,10 +9,11 @@ import cats.effect.Resource
import com.softwaremill.macwire._
import io.odin._
import nova.monadic_sfx.executors._
import nova.monadic_sfx.util.MediaPlayerResource
// import nova.monadic_sfx.util.IOUtils._
// import sttp.client.httpclient.monix.HttpClientMonixBackend
object Main extends MainModule with BIOApp {
lazy val schedulers = new Schedulers()
val schedulers = new Schedulers()
override def scheduler: Scheduler = schedulers.async
@ -25,6 +26,7 @@ object Main extends MainModule with BIOApp {
// toIO(HttpClientMonixBackend()(schedulers.async))
// )(c => toIO(c.close()))
// actorSystem <- actorSystemResource(logger)
MediaPlayerResource <- MediaPlayerResource()
_ <- Resource.liftF(wire[MainApp].program)
} yield ()

+ 257
- 131
src/main/scala/nova/monadic_sfx/MainApp.scala View File

@ -4,12 +4,15 @@ import java.util.concurrent.TimeUnit
import scala.util.Random
import com.jfoenix.controls.JFXDialog
import cats.effect.Resource
import cats.syntax.eq._
import cats.syntax.option._
import com.softwaremill.macwire._
import io.odin.Logger
import monix.bio.IO
import monix.bio.Task
import monix.eval.Coeval
import monix.execution.cancelables.CompositeCancelable
import monix.{eval => me}
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.implicits._
@ -18,31 +21,38 @@ import nova.monadic_sfx.ui.components.router.FXRouter
import nova.monadic_sfx.ui.components.router.Page
import nova.monadic_sfx.ui.components.todo.TodoListStore
import nova.monadic_sfx.ui.components.todo.TodoListView
import nova.monadic_sfx.util.MutHistory
import nova.monadic_sfx.util.MediaPlayerResource
import nova.monadic_sfx.util.controls.JFXButton
import nova.monadic_sfx.util.controls.JFXDialog
import nova.monadic_sfx.util.controls.JFXTextField
import nova.monadic_sfx.util.controls.VideoView
import org.gerweck.scalafx.util._
import org.kordamp.bootstrapfx.BootstrapFX
import scalafx.Includes._
import scalafx.application.JFXApp.PrimaryStage
import scalafx.application.JFXApp3.PrimaryStage
import scalafx.beans.property.ObjectProperty
import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer
import scalafx.geometry.Insets
import scalafx.geometry.Pos
import scalafx.scene.Node
import scalafx.scene.Parent
import scalafx.scene.Scene
import scalafx.scene.control.Button
import scalafx.scene.control.Label
import scalafx.scene.control.TableColumn
import scalafx.scene.control.TableView
import scalafx.scene.control.Tooltip
import scalafx.scene.layout.BorderPane
import scalafx.scene.layout.HBox
import scalafx.scene.layout.Priority
import scalafx.scene.layout.StackPane
import scalafx.util.Duration
class MainApp(
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
schedulers: Schedulers,
startTime: Long
startTime: Long,
mediaPlayer: MediaPlayerResource
)(implicit logger: Logger[Task]) {
private lazy val _scene = new Scene {
@ -60,153 +70,269 @@ class MainApp(
private lazy val stage = new PrimaryStage {
title = "Simple ScalaFX App"
scene = _scene
width = 640
height = 480
minWidth = 700
minHeight = 520
width = 1280
height = 720
// resizable = false
}
val program = for {
(fxApp, fxAppFib) <- wire[MyFxApp].init(stage)
_ <-
wire[MainAppDelegate].init
.flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
.executeOn(schedulers.fx)
_ <- Task(stage.resizable = false).executeOn(schedulers.fx)
currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS)
_ <- logger.info(
s"Application started in ${(currentTime - startTime) / 1000f} seconds"
)
// _ <- Task(CSSFX.start(stage))
_ <- fxAppFib.join
} yield ()
(for {
(stopSignal, fxAppFib) <- MyFxApp.resource(schedulers, stage)
i <- Resource.make(Task(1))(_ => Task.unit)
} yield (stopSignal, fxAppFib, i)).use {
case (a, b, c) => Task.unit
}
val program = MyFxApp
.resource(schedulers, stage)
.evalMap {
case (stopSignal, fxAppFib) =>
wire[MainAppDelegate].init
.flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
.executeOn(schedulers.fx) >> Task.pure(stopSignal -> fxAppFib)
}
.use {
case (stopSignal, fxAppFib) =>
for {
// _ <- Task(stage.resizable = false).executeOn(schedulers.fx)
currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS)
_ <- logger.info(
s"Application started in ${(currentTime - startTime) / 1000f} seconds"
)
// _ <- Task(CSSFX.start(stage))
_ <- fxAppFib.join
} yield ()
}
}
class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) {
class MainAppDelegate(
schedulers: Schedulers,
mediaPlayer: MediaPlayerResource,
_scene: Scene
)(implicit logger: Logger[Task]) {
val buttonStyle = """| -fx-padding: 0.7em 0.57em;
| -fx-font-size: 14px;
| -jfx-button-type: RAISED;
| -fx-background-color: rgb(77,102,204);
| -fx-pref-width: 200;
| -fx-text-fill: WHITE; """.stripMargin
val router = new FXRouter[Page]
val init =
for {
//FXRouter does not allocate mutable state so it's ok to use pure here
history <- Task(new MutHistory[Page](Page.Home))
router <- Task.pure(new FXRouter[Page](history))
routerStore <- router.store(Page.Home, logger)
todoStore <- TodoListStore(logger)
todoComponent <- TodoListView(todoStore)
resolver: PartialFunction[Page, Task[Parent]] = {
case Page.Home =>
Task(new Label {
styleClass ++= Seq("text-white")
text = "HomePage"
})
case Page.UserHome(id0) =>
Task(new Label {
styleClass ++= Seq("text-white")
text = s"User Home, Id = $id0"
// val players = mediaPlayerFactory.mediaPlayers
// val videoPlayerController = players.newEmbeddedMediaPlayer()
// mediaPlayer.controller.videoSurface.set(
// videoSurfaceForImageView(videoView)
// )
// val videoPlayerControllerCleanup =
// Task(videoPlayerController.controls().stop()) >>
// Task(videoPlayerController.release())
val videoPage = new BorderPane { pane =>
// val mp = new MediaPlayer(
// new Media(
// "https://download.oracle.com/otndocs/products/javafx/oow2010-2.flv"
// // "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_5mb.mp4"
// )
// ) {
// autoPlay = true
// }
// obs(pane).subscribe()(schedulers.async)
hgrow = Priority.Always
center = new VideoView(mediaPlayer.controller) {
alignmentInParent = Pos.Center
preserveRatio = true
// hgrow = Priority.Always
// this.prefWidth <== pane.prefWidth
fitWidth = _scene.width.value - 40
_scene.width
.map(_ - 40)
.onChange((_, _, value) => fitWidth.value = value)
// (new DoubleProperty).<==(_scene.width.map(_ - 10))
// fitWidth.<==(_scene.width.map(_ - 10))
}
// videoPlayerController.video().videoDimension().setSize()
padding = Insets(0, 0, 5, 0)
bottom = new HBox {
val mrl = new StringProperty
spacing = 5
children ++= Seq(
new JFXTextField {
text = "https://www.youtube.com/watch?v=0QKQlf8r7ls"
text ==> mrl
prefWidth = 100
minWidth = 80
},
new JFXButton {
text = "Play Video"
style = buttonStyle
onAction = _ => {
if (mediaPlayer.controller.media().isValid())
mediaPlayer.controller.controls().stop()
mediaPlayer.controller
.media()
// .play(
// "https://download.oracle.com/otndocs/products/javafx/oow2010-2.flv"
// )
// .play("https://www.youtube.com/watch?v=yZIummTz9mM")
.play(mrl.value)
}
},
new JFXButton {
text = "Resume"
style = buttonStyle
onAction = _ => mediaPlayer.controller.controls().play()
},
new JFXButton {
text = "Pause"
style = buttonStyle
onAction = _ => mediaPlayer.controller.controls().pause()
},
new JFXButton {
text = "Stop"
style = buttonStyle
tooltip = new Tooltip {
text = "Stop"
showDelay = Duration(200)
}
onAction = _ => mediaPlayer.controller.controls().stop()
},
new JFXButton {
text = "Get Status"
style = buttonStyle
onAction = _ => {
println(mediaPlayer.controller.status().state())
}
}
)
}
}
val init: Task[Node] = for {
routerStore <- router.store(Page.Home, logger)
todoStore <- TodoListStore(logger)
todoComponent <- TodoListView(todoStore)
resolver: PartialFunction[Page, Parent] = {
case Page.Home => videoPage
// engine.load("https://www.youtube.com/embed/qmlegXdlnqI")
// engine.load("https://youtube.com/embed/aqz-KE-bpKQ")
// engine.load("http://www.youtube.com/embed/IyaFEBI_L24")
case Page.UserHome =>
new Label {
styleClass ++= Seq("text-white")
text = s"User Home, Id = ${Random.nextInt()}"
}
case Page.Todo => todoComponent
}
routerNode: Node <-
Task
.deferAction(implicit s =>
Task(new HBox { box =>
alignment = Pos.Center
//TODO find a better way to do this
// videoView.fitWidth <== box.prefWidth
children <-- router
.render(resolver)(routerStore)
// call cancel on the old component to cancel all subscriptions
.scan[Parent](new Label("empty")) { case (a, b) => b }
.doOnNextF(s => logger.debug(s"Actual receive: $s"))
.map(_.delegate)
})
case Page.Todo =>
Task.pure(todoComponent)
}
routerNode <-
Task
.deferAction(implicit s =>
Task(new HBox {
alignment = Pos.Center
//TODO find a better way to do this
var oldValue: Option[Parent] = None
children <-- router
.render(resolver)(routerStore)
// .scanEvalF[Coeval, (Option[Parent], Option[Parent])](
// Coeval.pure(None -> None)
// ) {
// case (oldValue, newValue) => Coeval(None -> None)
// }
// call cancel on the old component to cancel all subscriptions
.doOnNextF(newValue =>
Coeval { oldValue.foreach(_ => ()) } >> Coeval {
oldValue = Some(newValue)
}
)
.map(_.delegate)
})
)
)
mainSceneNode <- Task.deferAction(implicit s =>
Task(new StackPane { root =>
mainSceneNode <- Task.deferAction(implicit s =>
Task(new StackPane { root =>
alignment = Pos.Center
hgrow = Priority.Always
vgrow = Priority.Always
children = new BorderPane {
hgrow = Priority.Always
vgrow = Priority.Always
children = new BorderPane {
hgrow = Priority.Always
vgrow = Priority.Always
center = routerNode
bottom = new HBox {
alignment = Pos.Center
spacing = 20
children = Seq(
new JFXButton {
text = "Forward"
style = buttonStyle
onAction = () => {
history.forward()
routerStore.onNext(FXRouter.HistoryEvent(history.current))
}
},
new JFXButton {
text = "Backward"
style = buttonStyle
onAction = () => {
history.backward()
routerStore.onNext(FXRouter.HistoryEvent(history.current))
center = routerNode
bottom = new HBox {
implicit val cc = CompositeCancelable()
alignment = Pos.Center
spacing = 20
children = Seq(
new JFXButton {
text = "Forward"
style = buttonStyle
obsAction.useLazyEval(
me.Task.pure(FXRouter.Forward)
) --> routerStore
disable <-- routerStore.map {
case (_, FXRouter.State(_, h)) =>
h.state.sp == h.state.values.size - 1
}
},
new JFXButton {
text = "Backward"
style = buttonStyle
obsAction.useLazyEval(
me.Task(println("Fired")) >> me.Task.pure(FXRouter.Backward)
) --> routerStore
disable <-- routerStore
.doOnNextF(b => Coeval(println(s"Received1: $b")))
.map {
case (_, FXRouter.State(_, h)) => h.state.sp == 0
}
},
new JFXButton {
text = "Home"
style = buttonStyle
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.Home))
) --> routerStore
},
new JFXButton {
text = "Todo"
style = buttonStyle
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.Todo))
) --> routerStore
},
new JFXButton {
text = "UserHome"
style = buttonStyle
obsAction
.useLazyEval(
me.Task(
FXRouter.Replace(Page.UserHome(Random.nextInt(20)))
)
) --> routerStore
},
new JFXButton {
text = "Dialog"
style = buttonStyle
val d = new JFXDialog()
d.setContent(new HBox {
children = Seq(new Label("hmm"))
},
new JFXButton {
text = "Home"
style = buttonStyle
disable <-- routerStore
.map { case (_, FXRouter.State(p, _)) => p === Page.Home }
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.Home))
) --> routerStore
},
new JFXButton {
text = "Todo"
style = buttonStyle
disable <-- routerStore
.map { case (_, FXRouter.State(p, _)) => p === Page.Todo }
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.Todo))
) --> routerStore
},
new JFXButton {
text = "UserHome"
style = buttonStyle
disable <-- routerStore
.map { case (_, FXRouter.State(p, _)) => p == Page.UserHome }
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.UserHome))
) --> routerStore
},
new JFXButton {
text = "Dialog"
style = buttonStyle
val d = new JFXDialog {
content = new HBox {
style = "-fx-background-color: black"
children = Seq(new Label {
styleClass ++= Seq("text-white")
text = "Sample Dialog"
})
padding = Insets(20)
})
d.styleClass ++= Seq("text-white")
onAction = () => d.show(root)
}
}
)
}
onAction = () => d.show(root)
}
)
}
})
)
} yield mainSceneNode
}
})
)
} yield mainSceneNode
}
class TestModel(_name: String, _age: Int) {

+ 5
- 5
src/main/scala/nova/monadic_sfx/MainModule.scala View File

@ -19,23 +19,23 @@ trait MainModule extends ActorModule with UiModule with HttpModule {
"nova.monadic_sfx.util.reactive.store.Store" -> storeLogger
)
.withFallback(defaultLogger)
.withAsync(timeWindow = 1.millis)
.withAsync(timeWindow = 10.millis)
def makeLogger =
for {
defaultLogger <- consoleLogger[Task]()
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
.withAsync(timeWindow = 10.millis) |+| fileLogger[Task](
"application.log"
).withAsync(timeWindow = 1.millis)
).withAsync(timeWindow = 10.millis)
middlewareLogger <-
consoleLogger[
Task
](formatter = Middlewares.format)
.withMinimalLevel(Level.Trace)
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
.withAsync(timeWindow = 10.millis) |+| fileLogger[Task](
"stores.log",
formatter = Middlewares.format
).withAsync(timeWindow = 1.millis)
).withAsync(timeWindow = 10.millis)
routerLogger <- routerLogger(defaultLogger, middlewareLogger)
} yield (routerLogger)
}

+ 2
- 0
src/main/scala/nova/monadic_sfx/Types.scala View File

@ -10,5 +10,7 @@ import sttp.client.httpclient.WebSocketHandler
trait AppTypes {
type HttpBackend =
SttpBackend[Task, Observable[ByteBuffer], WebSocketHandler]
type CrossBackend =
SttpBackend[Task, Observable[ByteBuffer], Nothing]
}
object AppTypes extends AppTypes {}

+ 8
- 5
src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala View File

@ -42,7 +42,7 @@ trait JavaFXMonixObservables {
) =
new ReadOnlyPropertyExt[T, J](prop)
implicit def extendedObservableList[A](
list: ObservableList[A]
list: ObservableBuffer[A]
) = new ObservableListExt(list)
implicit def extendedStringObservableList(
list: ObservableList[String]
@ -92,7 +92,7 @@ object JavaFXMonixObservables {
}
}
scene.onMouseDragged = l
scene.onMouseDragged = l;
c := Cancelable(() =>
scene.removeEventHandler(
jfxsi.MouseEvent.MOUSE_DRAGGED,
@ -115,7 +115,7 @@ object JavaFXMonixObservables {
}
def <--(
obs: Observable[_ <: T]
obs: Observable[T]
)(implicit s: Scheduler, c: CompositeCancelable): Unit = {
c += obs.doOnNextF(v => Coeval(prop.value = v)).subscribe()
}
@ -142,7 +142,10 @@ object JavaFXMonixObservables {
extends AnyVal {
def -->(sub: Observer[A]) =
prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
prop.onChange((a, b, c) =>
if (c != null)
if (sub.onNext(c) == Ack.Stop) throw new Exception("boom")
)
def ==>(op: Property[A, A]) = {
prop.onChange((a, b, c) => if (c != null) op() = c)
@ -168,7 +171,7 @@ object JavaFXMonixObservables {
}
final class ObservableListExt[A](
private val buffer: ObservableList[A]
private val buffer: ObservableBuffer[A]
) extends AnyVal {
// def -->(sub: Observer[A]) =

+ 10
- 1
src/main/scala/nova/monadic_sfx/implicits/package.scala View File

@ -1,8 +1,17 @@
package nova.monadic_sfx
import monix.eval.TaskLike
import monix.reactive.Observable
package object implicits
extends MySfxObservableImplicits
with JavaFXMonixObservables
with JavaFXMonixObservables {
implicit class SttpWsOps[F[_]](private val ws: sttp.client.ws.WebSocket[F])
extends AnyVal {
def observableSource(implicit F: TaskLike[F]) =
Observable.repeatEvalF(ws.receiveText())
}
}
// implicit class NodeExt(val node: Node) {
// def lookup2[T <: SFXDelegate[_]](

+ 71
- 0
src/main/scala/nova/monadic_sfx/ui/FXComponent.scala View File

@ -0,0 +1,71 @@
package nova.monadic_sfx.ui
import javafx.{scene => jfxc}
import monix.bio.Task
import monix.execution.Cancelable
import monix.execution.cancelables.CompositeCancelable
import monix.reactive.subjects.ConcurrentSubject
import nova.monadic_sfx.implicits._
import scalafx.beans.property.ObjectProperty
import scalafx.scene.Node
import scalafx.scene.Parent
import scalafx.scene.control.TextField
import scalafx.scene.text.Font
import cats.effect.Resource
final class FXComponent private (
val rootNode: Parent,
val cancelable: Cancelable
)
object FXComponent {
def acquire(f: CompositeCancelable => Task[Parent]) =
for {
c <- Task(CompositeCancelable())
p <- f(c)
} yield new FXComponent(p, c)
def fxComponent2Node(
component: FXComponent
)(implicit c: CompositeCancelable): Node = {
c += component.cancelable
component.rootNode
}
def resource(f: CompositeCancelable => Task[Parent]) =
Resource.make(acquire(f))(comp => Task(comp.cancelable.cancel()))
}
object TestFXComp {
val testComp =
FXComponent.resource { implicit c =>
Task.deferAction { implicit s =>
Task {
val sub = ConcurrentSubject.publish[jfxc.text.Font]
val f = ObjectProperty(Font("hmm"))
sub.onNext(f())
new TextField {
font <-- sub
font <== f
}
}
}
}
// val x = for {
// comp <- testComp
// res <- FXComponent.make { implicit c =>
// Task.deferAction { implicit s =>
// Task {
// new BorderPane {
// val sub = ConcurrentSubject.publish[jfxc.text.Font]
// center = FXComponent.fxComponent2Node(comp)
// bottom = new TextField {
// font <-- sub
// }
// }
// }
// }
// }
// } yield res
}

+ 42
- 24
src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala View File

@ -2,41 +2,59 @@ package nova.monadic_sfx.ui
import scala.concurrent.duration._
import cats.effect.Resource
import io.odin.Logger
import monix.bio.Fiber
import monix.bio.Task
import monix.execution.CancelablePromise
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.ui.DefaultUI
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.application.JFXApp3
import scalafx.application.JFXApp3.PrimaryStage
class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
private lazy val internal = new JFXApp {
stage = new PrimaryStage {
scene = DefaultUI.scene
private def internal(
startSignal: CancelablePromise[Unit],
stopSignal: CancelablePromise[Unit]
) =
new JFXApp3 {
def start(): Unit = {
stage = new PrimaryStage {
scene = DefaultUI.scene
}
startSignal.success(())
}
override def stopApp(): Unit = {
stopSignal.success(())
}
}
}
// def stage = Task(internal.stage)
// def stage_=(stage: PrimaryStage) = Task(internal.stage = stage)
// def useInternal[T](f: JFXApp => Task[T]): Task[T] =
// for {
// _ <- logger.debug("Request for using internal value")
// res <- f(internal).executeOn(schedulers.fx)
// _ <- logger.debug(s"Result was ${res.toString()}")
// } yield (res)
}
def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) =
for {
object MyFxApp {
def resource(
schedulers: Schedulers,
stage: => PrimaryStage,
transitionDelay: FiniteDuration = 500.millis
)(implicit
logger: Logger[Task]
): Resource[Task, (Task[Unit], Fiber[Throwable, Unit])] =
Resource.make(for {
_ <- logger.info("Starting FX App")
fxApp <- Task(new MyFxApp(schedulers))
startSignal <- Task(CancelablePromise[Unit]())
stopSignal <- Task(CancelablePromise[Unit]())
delegate <- Task(fxApp.internal(startSignal, stopSignal))
fib <-
Task(internal.main(Array.empty)).start.executeOn(schedulers.blocking)
_ <- Task.sleep(200.millis)
_ <- Task(internal.stage = stage)
Task(delegate.main(Array.empty)).start.executeOn(schedulers.blocking)
_ <- Task.fromCancelablePromise(startSignal)
_ <- Task.sleep(transitionDelay)
_ <- Task(delegate.stage = stage)
.executeOn(schedulers.fx)
.delayExecution(delay)
} yield (this, fib)
.delayExecution(transitionDelay)
} yield Task.fromCancelablePromise(stopSignal) -> fib) {
case _ -> fib => fib.cancel
}
}

+ 34
- 15
src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala View File

@ -2,6 +2,9 @@ package nova.monadic_sfx.ui.components.router
import scala.concurrent.duration._
import cats.kernel.Eq
import cats.syntax.eq._
import com.softwaremill.quicklens._
import io.circe.Codec
import io.circe.Decoder
import io.circe.Encoder
@ -11,8 +14,8 @@ import io.odin.Logger
import monix.bio.Task
import monix.eval.Coeval
import monix.reactive.Observable
import nova.monadic_sfx.util.History
import nova.monadic_sfx.util.IOUtils
import nova.monadic_sfx.util.MutHistory
import nova.monadic_sfx.util.controls.JFXSpinner
import nova.monadic_sfx.util.reactive.store.Middlewares
import nova.monadic_sfx.util.reactive.store.Reducer
@ -21,7 +24,10 @@ import scalafx.scene.Parent
object FXRouter {
final case class State[P](page: P)
final case class State[P](page: P, history: History[P])
object State {
implicit def eqForAction[T] = Eq.fromUniversalEquals[State[T]]
}
sealed abstract class Action[+T]
final case class Replace[T](page: T) extends Action[T]
@ -31,16 +37,14 @@ object FXRouter {
object Action {
implicit def codec[T: Encoder: Decoder]: Codec[Action[T]] = deriveCodec
implicit def eqForAction[T] = Eq.fromUniversalEquals[Action[T]]
}
type FXStore[P] = Store[Action[P], State[P]]
}
class FXRouter[P](history: MutHistory[P])(implicit
E: Encoder[P],
D: Decoder[P]
) {
class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
import FXRouter._
def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
@ -52,9 +56,10 @@ class FXRouter[P](history: MutHistory[P])(implicit
)
store <- Store.createL[Action[P], State[P]](
Replace(initialPage),
State(initialPage),
State(initialPage, History(initialPage)),
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _),
Seq(mw)
// Seq(classOf[HistoryEvent[P]])
)
} yield store
)
@ -66,27 +71,39 @@ class FXRouter[P](history: MutHistory[P])(implicit
action match {
// case Init => (state, None)
case Replace(p) =>
history.push(p)
(state.copy(page = p), None)
(state.copy(page = p, history = state.history :+ p), None)
case HistoryEvent(p) =>
(state.copy(page = p), None)
case Forward => (state, None)
case Backward => (state, None)
case Forward =>
val s1 = state.modify(_.history).using(_.forward)
val s2 = s1.modify(_.page).setTo(s1.history.current)
s2 -> Some(Task.pure(HistoryEvent(s2.history.current)))
case Backward =>
val s1 = state.modify(_.history).using(_.backward)
val s2 = s1.modify(_.page).setTo(s1.history.current)
s2 -> Some(Task.pure(HistoryEvent(s2.history.current)))
}
def render(
resolver: P => Task[Parent],
resolver: P => Parent,
transitionDelay: FiniteDuration = 500.millis
)(implicit store: FXStore[P]) =
store
.filter {
case (a, _) => a =!= FXRouter.Forward
}
.filter {
case (a, _) => a =!= FXRouter.Backward
}
.distinctUntilChanged
.flatMap {
case (_, FXRouter.State(p)) =>
case (_, FXRouter.State(p, _)) =>
Observable.from(Coeval(new JFXSpinner)) ++ Observable.from(
IOUtils.toTask(
Task
.racePair(
Task.sleep(transitionDelay),
resolver(p)
Task.pure(resolver(p))
)
.flatMap {
case Left(_ -> fib) => fib.join
@ -108,8 +125,10 @@ class FXRouter[P](history: MutHistory[P])(implicit
sealed trait Page
object Page {
final case object Home extends Page
final case class UserHome(id: Int) extends Page
final case object UserHome extends Page
final case object Todo extends Page
implicit val eqForPage = Eq.fromUniversalEquals[Page]
}
// case class State()

+ 10
- 13
src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala View File

@ -8,12 +8,10 @@ import monix.bio.Task
import monix.catnap.ConcurrentChannel
import monix.catnap.ConsumerF
import monix.execution.Scheduler
import monix.reactive.Observable
import monix.reactive.Observer
import nova.monadic_sfx.util.controls.FontIcon
import nova.monadic_sfx.util.controls.IconLiteral
import nova.monadic_sfx.util.controls.JFXListView
import nova.monadic_sfx.implicits._
import nova.monadic_sfx.util.reactive.store._
import scalafx.Includes._
import scalafx.beans.property.StringProperty
@ -57,13 +55,12 @@ object TodoListViewOld {
): Task[JFXListView[Todo]] =
Task.deferAction(implicit s =>
Task {
val todos =
store.map { case (_, items) => items }
val todos = store.map { case (_, items) => items }
val listView = new JFXListView[Todo] { lv =>
def selectedItems = lv.selectionModel().selectedItems.view
// items = todos
items <-- todos
// items <-- todos
// .map(ObservableBuffer.from(_))
cellFactory = _ =>
new ListCell[Todo] {
@ -216,14 +213,14 @@ object TodoListComponentOld {
case _ => state
}
val store =
Store
.createL[TodoListComponentOld.Command, Vector[Todo]](
TodoListComponentOld.Delete(0),
Vector.empty[Todo],
(s: Vector[Todo], a: TodoListComponentOld.Command) =>
reducer(s, a) -> Observable.empty
)
// val store =
// Store
// .createL[TodoListComponentOld.Command, Vector[Todo]](
// TodoListComponentOld.Delete(0),
// Vector.empty[Todo],
// (s: Vector[Todo], a: TodoListComponentOld.Command) =>
// reducer(s, a) -> Observable.empty
// )
class Props(
val todoListView: TodoListViewOld,

+ 23
- 12
src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala View File

@ -1,5 +1,6 @@
package nova.monadic_sfx.ui.components.todo
import cats.kernel.Eq
import com.softwaremill.quicklens._
import io.circe.generic.JsonCodec
import io.odin.Logger
@ -9,6 +10,9 @@ import nova.monadic_sfx.util.reactive.store.Reducer
import nova.monadic_sfx.util.reactive.store.Store
case class Todo(id: Int, content: String)
object Todo {
implicit val eqForTodo = Eq.fromUniversalEquals[Todo]
}
object TodoListStore {
@ -21,8 +25,15 @@ object TodoListStore {
private case class InternalAdd(content: String) extends Action
private case object End extends Action
object Action {
implicit val eqForAction = Eq.fromUniversalEquals[Action]
}
case class State(todos: Vector[Todo], counter: Int)
object State {
implicit val eqForState = Eq.fromUniversalEquals[State]
}
def reducer(logger: Logger[Task])(
state: State,
@ -32,12 +43,13 @@ object TodoListStore {
case Init => (state, None)
case Add(content) =>
val nextAction = Some(for {
// do some validation
// _ <- logger.debug(s"Received $content")
res <- Task.pure(InternalAdd(content))
} yield res)
(state, nextAction)
case Edit(_id, content) =>
val condition: Todo => Boolean = _.id == _id
case Edit(id, content) =>
val condition: Todo => Boolean = _.id == id
val nextState = state
.modify(_.todos.eachWhere(condition))
.using(_.copy(content = content))
@ -46,15 +58,17 @@ object TodoListStore {
(state.copy(state.todos.filterNot(_.id == id)), None)
case InternalAdd(content) =>
val nextState = state.copy(
todos = state.todos :+ Todo(state.counter, content),
counter = state.counter + 1
)
val nextState =
state
.modify(_.todos)
.using(_ :+ Todo(state.counter, content))
.modify(_.counter)
.using(_ + 1)
(nextState, Some(logger.debug(s"Received $content") >> Task.pure(End)))
case End => (state, None)
}
def apply(logger: Logger[Task]) =
def apply(logger: Logger[Task]): Task[Store[Action, State]] =
Task.deferAction(implicit s =>
for {
logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore")
@ -64,12 +78,9 @@ object TodoListStore {
Init,
State(Vector.empty[Todo], 0),
Reducer.withOptionalEffects(reducer(logger) _),
Seq(
// actionLoggerMiddleware(logger, "TodoStore2")
logMware
)
Seq(logMware)
)
} yield (store)
} yield store
)
}

+ 33
- 12
src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala View File

@ -1,30 +1,33 @@
package nova.monadic_sfx.ui.components.todo
import monix.bio.Task
import monix.eval.Coeval
import monix.execution.cancelables.CompositeCancelable
import monix.{eval => me}
import nova.monadic_sfx.implicits._
import nova.monadic_sfx.util.controls.FontIcon
import nova.monadic_sfx.util.controls.IconLiteral
import nova.monadic_sfx.util.controls.JFXButton
import nova.monadic_sfx.util.controls.JFXListView
import nova.monadic_sfx.util.controls.JFXTextField
import nova.monadic_sfx.util.controls.MenuItem
import nova.monadic_sfx.implicits._
import nova.monadic_sfx.util.reactive.store._
import org.gerweck.scalafx.util._
import scalafx.Includes._
import scalafx.beans.property.ObjectProperty
import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer
import scalafx.geometry.Insets
import scalafx.geometry.Pos
import scalafx.scene.Parent
import scalafx.scene.control.ContextMenu
import scalafx.scene.control.Label
import scalafx.scene.control.ListCell
import scalafx.scene.control.SelectionMode
import scalafx.scene.layout.HBox
import scalafx.scene.text.Text
import scalafx.geometry.Pos
import scalafx.scene.layout.BorderPane
import scalafx.scene.layout.HBox
import scalafx.scene.layout.Priority
import scalafx.scene.paint.Color
object TodoListView {
def apply(
@ -32,23 +35,29 @@ object TodoListView {
): Task[Parent] =
Task.deferAction(implicit s =>
Task {
val cc = CompositeCancelable()
val todos = store.map { case (_, state) => state.todos }
implicit val cc = CompositeCancelable()
val todos = store
.map { case (_, state) => state.todos }
.distinctUntilChanged
.map(ObservableBuffer.from)
.doOnNextF(item => Coeval(println(s"Received item: $item")))
val _selectedItems = ObjectProperty(Seq.empty[Todo])
new BorderPane {
padding = Insets(5)
hgrow = Priority.Always
val _content = StringProperty("")
center = new HBox {
padding = Insets(5)
children ++= Seq(new JFXListView[Todo] {
id = "todoList"
hgrow = Priority.Always