cleanup/refactorings
This commit is contained in:
parent
672851aeeb
commit
5b0cfcae8c
@ -15,9 +15,7 @@ resolvers += "jitpack" at "https://jitpack.io"
|
|||||||
enablePlugins(JavaFxPlugin)
|
enablePlugins(JavaFxPlugin)
|
||||||
|
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.typelevel" %% "cats-core" % "2.2.0",
|
"io.monix" %% "monix" % "3.4.0",
|
||||||
"org.typelevel" %% "cats-effect" % "2.2.0",
|
|
||||||
"io.monix" %% "monix" % "3.3.0",
|
|
||||||
"io.monix" %% "monix-bio" % "1.1.0",
|
"io.monix" %% "monix-bio" % "1.1.0",
|
||||||
"io.circe" %% "circe-core" % "0.13.0",
|
"io.circe" %% "circe-core" % "0.13.0",
|
||||||
"io.circe" %% "circe-generic" % "0.13.0",
|
"io.circe" %% "circe-generic" % "0.13.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
javafx-dispatcher {
|
javafx-dispatcher {
|
||||||
type = "Dispatcher"
|
type = "Dispatcher"
|
||||||
executor = "nova.monadic_sfx.executors.JavaFXEventThreadExecutorServiceConfigurator"
|
executor = "nova.monadic_sfx.concurrent.JavaFXEventThreadExecutorServiceConfigurator"
|
||||||
throughput = 1
|
throughput = 1
|
||||||
}
|
}
|
||||||
akka.jvm-exit-on-fatal-error = on
|
akka.jvm-exit-on-fatal-error = on
|
350
src/main/scala/nova/monadic_sfx/App.scala
Normal file
350
src/main/scala/nova/monadic_sfx/App.scala
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
package nova.monadic_sfx
|
||||||
|
|
||||||
|
import cats.syntax.eq._
|
||||||
|
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.concurrent.Schedulers
|
||||||
|
import nova.monadic_sfx.implicits._
|
||||||
|
import nova.monadic_sfx.ui.FX
|
||||||
|
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.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.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.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
|
||||||
|
import nova.monadic_sfx.util.controls.ActionObservableDsl
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import scala.util.Random
|
||||||
|
import cats.effect.Resource
|
||||||
|
import nova.monadic_sfx.ui.FXComponent
|
||||||
|
|
||||||
|
object App {
|
||||||
|
|
||||||
|
val _scene = Coeval(new Scene {
|
||||||
|
root = new HBox {
|
||||||
|
padding = Insets(20)
|
||||||
|
// style = """| -fx-background-color: rgb(38, 38, 38);
|
||||||
|
// | -fx-text-fill: white;""".stripMargin
|
||||||
|
stylesheets ++= Seq(
|
||||||
|
BootstrapFX.bootstrapFXStylesheet,
|
||||||
|
os.rel / "static" / "css" / "main.css"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val stage = Coeval(new PrimaryStage {
|
||||||
|
title = "Simple ScalaFX App"
|
||||||
|
scene = _scene.value()
|
||||||
|
minWidth = 700
|
||||||
|
minHeight = 520
|
||||||
|
width = 1280
|
||||||
|
height = 720
|
||||||
|
// resizable = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
final class App(
|
||||||
|
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
||||||
|
fx: FX,
|
||||||
|
schedulers: Schedulers,
|
||||||
|
startTime: Long,
|
||||||
|
mediaPlayer: MediaPlayerResource
|
||||||
|
)(implicit logger: Logger[Task]) {
|
||||||
|
|
||||||
|
// val program = MyFxApp
|
||||||
|
// .resource(schedulers, stage)
|
||||||
|
// .evalMap { fxApp =>
|
||||||
|
// new MainAppDelegate(schedulers, mediaPlayer, _scene).init
|
||||||
|
// .flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
|
||||||
|
// .executeOn(schedulers.fx) >> Task.pure(fxApp)
|
||||||
|
// }
|
||||||
|
// .use { fxApp =>
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
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 players = mediaPlayerFactory.mediaPlayers
|
||||||
|
// val videoPlayerController = players.newEmbeddedMediaPlayer()
|
||||||
|
// mediaPlayer.controller.videoSurface.set(
|
||||||
|
// videoSurfaceForImageView(videoView)
|
||||||
|
// )
|
||||||
|
|
||||||
|
// val videoPlayerControllerCleanup =
|
||||||
|
// Task(videoPlayerController.controls().stop()) >>
|
||||||
|
// Task(videoPlayerController.release())
|
||||||
|
|
||||||
|
val videoPage = Coeval {
|
||||||
|
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 = for {
|
||||||
|
routerStore <- Resource.eval(router.store2(Page.Home, logger))
|
||||||
|
todoStore <- Resource.eval(TodoListStore(logger))
|
||||||
|
todoComponent <- TodoListView(todoStore)
|
||||||
|
videoPage <- Resource.eval(videoPage.to[Task])
|
||||||
|
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.node
|
||||||
|
}
|
||||||
|
routerNode <- FXComponent(implicit cc =>
|
||||||
|
Task
|
||||||
|
.deferAction(implicit s =>
|
||||||
|
Task(new HBox { box =>
|
||||||
|
// implicit val cc = CompositeCancelable()
|
||||||
|
alignment = Pos.Center
|
||||||
|
//TODO find a better way to do this
|
||||||
|
// videoView.fitWidth <== box.prefWidth
|
||||||
|
children <-- router
|
||||||
|
.render2(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)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
mainSceneNode <- FXComponent(implicit cc =>
|
||||||
|
Task.deferAction(implicit s =>
|
||||||
|
Task(new StackPane { root =>
|
||||||
|
import ActionObservableDsl._
|
||||||
|
alignment = Pos.Center
|
||||||
|
hgrow = Priority.Always
|
||||||
|
vgrow = Priority.Always
|
||||||
|
children = new BorderPane {
|
||||||
|
hgrow = Priority.Always
|
||||||
|
vgrow = Priority.Always
|
||||||
|
center = routerNode.node
|
||||||
|
bottom = new HBox {
|
||||||
|
// implicit val cc = CompositeCancelable()
|
||||||
|
alignment = Pos.Center
|
||||||
|
spacing = 20
|
||||||
|
children = Seq(
|
||||||
|
new JFXButton {
|
||||||
|
text = "Forward"
|
||||||
|
style = buttonStyle
|
||||||
|
obsAction.map(_ => FXRouter.Forward) --> routerStore.sink
|
||||||
|
disable <-- routerStore.source.map {
|
||||||
|
case (_, FXRouter.State(_, h)) =>
|
||||||
|
h.state.sp == h.state.values.size - 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new JFXButton {
|
||||||
|
text = "Backward"
|
||||||
|
style = buttonStyle
|
||||||
|
|
||||||
|
obsAction.mapEval(_ =>
|
||||||
|
me.Task(println("Fired")) >> me.Task.pure(FXRouter.Backward)
|
||||||
|
) --> routerStore.sink
|
||||||
|
disable <-- routerStore.source
|
||||||
|
.doOnNextF(b => Coeval(println(s"Received1: $b")))
|
||||||
|
.map {
|
||||||
|
case (_, FXRouter.State(_, h)) => h.state.sp == 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new JFXButton {
|
||||||
|
text = "Home"
|
||||||
|
style = buttonStyle
|
||||||
|
disable <-- routerStore.source
|
||||||
|
.map { case (_, FXRouter.State(p, _)) => p === Page.Home }
|
||||||
|
obsAction.map(_ =>
|
||||||
|
FXRouter.Replace(Page.Home)
|
||||||
|
) --> routerStore.sink
|
||||||
|
},
|
||||||
|
new JFXButton {
|
||||||
|
text = "Todo"
|
||||||
|
style = buttonStyle
|
||||||
|
disable <-- routerStore.source
|
||||||
|
.map { case (_, FXRouter.State(p, _)) => p === Page.Todo }
|
||||||
|
obsAction.map(_ =>
|
||||||
|
FXRouter.Replace(Page.Todo)
|
||||||
|
) --> routerStore.sink
|
||||||
|
},
|
||||||
|
new JFXButton {
|
||||||
|
text = "UserHome"
|
||||||
|
style = buttonStyle
|
||||||
|
disable <-- routerStore.source
|
||||||
|
.map {
|
||||||
|
case (_, FXRouter.State(p, _)) => p === Page.UserHome
|
||||||
|
}
|
||||||
|
obsAction.map(_ =>
|
||||||
|
FXRouter.Replace(Page.UserHome)
|
||||||
|
) --> routerStore.sink
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onAction = () => d.show(root)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} yield mainSceneNode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TestModel(_name: String, _age: Int) {
|
||||||
|
val name = StringProperty(_name).readOnly
|
||||||
|
val age = ObjectProperty(_age).readOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
object Test {
|
||||||
|
val items = ObservableBuffer(
|
||||||
|
new TestModel("hmm", 1),
|
||||||
|
new TestModel("hmm2", 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
val ttv = new TableView[TestModel](items) {
|
||||||
|
columns ++= Seq(
|
||||||
|
new TableColumn[TestModel, String] {
|
||||||
|
text = "Name"
|
||||||
|
cellValueFactory = { _.value.name }
|
||||||
|
},
|
||||||
|
new TableColumn[TestModel, Int] {
|
||||||
|
text = "Age"
|
||||||
|
cellValueFactory = { _.value.age }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -7,12 +7,9 @@ import cats.implicits._
|
|||||||
import io.odin._
|
import io.odin._
|
||||||
import io.odin.config._
|
import io.odin.config._
|
||||||
import io.odin.syntax._
|
import io.odin.syntax._
|
||||||
import nova.monadic_sfx.actors.ActorModule
|
|
||||||
import nova.monadic_sfx.http.HttpModule
|
|
||||||
import nova.monadic_sfx.ui.UiModule
|
|
||||||
import nova.monadic_sfx.util.reactive.store.Middlewares
|
import nova.monadic_sfx.util.reactive.store.Middlewares
|
||||||
|
|
||||||
trait MainModule extends ActorModule with UiModule with HttpModule {
|
object Loggers {
|
||||||
def routerLogger(defaultLogger: Logger[Task], storeLogger: Logger[Task]) =
|
def routerLogger(defaultLogger: Logger[Task], storeLogger: Logger[Task]) =
|
||||||
enclosureRouting[Task](
|
enclosureRouting[Task](
|
||||||
"nova.monadic_sfx.util.reactive.store.Middlewares" -> storeLogger,
|
"nova.monadic_sfx.util.reactive.store.Middlewares" -> storeLogger,
|
||||||
@ -37,5 +34,5 @@ trait MainModule extends ActorModule with UiModule with HttpModule {
|
|||||||
formatter = Middlewares.format
|
formatter = Middlewares.format
|
||||||
).withAsync(timeWindow = 10.millis)
|
).withAsync(timeWindow = 10.millis)
|
||||||
routerLogger <- routerLogger(defaultLogger, middlewareLogger)
|
routerLogger <- routerLogger(defaultLogger, middlewareLogger)
|
||||||
} yield (routerLogger)
|
} yield routerLogger
|
||||||
}
|
}
|
@ -1,37 +1,52 @@
|
|||||||
package nova.monadic_sfx
|
package nova.monadic_sfx
|
||||||
|
|
||||||
import _root_.monix.bio.BIOApp
|
|
||||||
import _root_.monix.bio.Task
|
|
||||||
import _root_.monix.bio.UIO
|
|
||||||
import _root_.monix.execution.Scheduler
|
|
||||||
import cats.effect.ExitCode
|
import cats.effect.ExitCode
|
||||||
import cats.effect.Resource
|
import cats.effect.Resource
|
||||||
import com.softwaremill.macwire._
|
import monix.bio.BIOApp
|
||||||
import io.odin._
|
import monix.bio.Task
|
||||||
import nova.monadic_sfx.executors._
|
import monix.bio.UIO
|
||||||
|
import monix.execution.Scheduler
|
||||||
|
import nova.monadic_sfx.concurrent._
|
||||||
import nova.monadic_sfx.util.MediaPlayerResource
|
import nova.monadic_sfx.util.MediaPlayerResource
|
||||||
|
import nova.monadic_sfx.ui.FX
|
||||||
// import nova.monadic_sfx.util.IOUtils._
|
// import nova.monadic_sfx.util.IOUtils._
|
||||||
// import sttp.client.httpclient.monix.HttpClientMonixBackend
|
// import sttp.client.httpclient.monix.HttpClientMonixBackend
|
||||||
object Main extends MainModule with BIOApp {
|
object Main extends BIOApp {
|
||||||
val schedulers = new Schedulers()
|
val schedulers = Schedulers.default
|
||||||
|
|
||||||
override def scheduler: Scheduler = schedulers.async
|
override def scheduler: Scheduler = schedulers.async.value
|
||||||
|
|
||||||
def appResource(startTime: Long) =
|
|
||||||
for {
|
|
||||||
implicit0(logger: Logger[Task]) <- makeLogger
|
|
||||||
|
|
||||||
|
val appResource = for {
|
||||||
|
startTime <- Resource.eval(Task(System.currentTimeMillis()))
|
||||||
|
logger <- Loggers.makeLogger
|
||||||
// backend and actorsystem are for future use
|
// backend and actorsystem are for future use
|
||||||
// backend <- Resource.make(
|
// backend <- Resource.make(
|
||||||
// toIO(HttpClientMonixBackend()(schedulers.async))
|
// toIO(HttpClientMonixBackend()(schedulers.async))
|
||||||
// )(c => toIO(c.close()))
|
// )(c => toIO(c.close()))
|
||||||
// actorSystem <- actorSystemResource(logger)
|
// actorSystem <- actorSystemResource(logger)
|
||||||
MediaPlayerResource <- MediaPlayerResource()
|
fx <- FX.resource(schedulers, App.stage)(logger)
|
||||||
_ <- Resource.liftF(wire[MainApp].program)
|
mediaPlayerResource <- MediaPlayerResource()
|
||||||
|
rootComponent = new App(fx, schedulers, startTime, mediaPlayerResource)(
|
||||||
|
logger
|
||||||
|
).init
|
||||||
|
_ <- Resource.eval(
|
||||||
|
rootComponent
|
||||||
|
.evalMap(c =>
|
||||||
|
for {
|
||||||
|
_ <- Task.unit
|
||||||
|
_ <- fx.addToScene(UIO.pure(c.node))
|
||||||
|
_ <- fx.await
|
||||||
|
} yield ()
|
||||||
|
)
|
||||||
|
.use(_ => Task.unit)
|
||||||
|
.executeOn(schedulers.fx.value)
|
||||||
|
)
|
||||||
|
// _ <- Resource.eval(f)
|
||||||
|
// _ <- Resource.eval(fx.await)
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
override def run(args: List[String]): UIO[ExitCode] =
|
override def run(args: List[String]): UIO[ExitCode] =
|
||||||
appResource(System.currentTimeMillis())
|
appResource
|
||||||
.use(_ => Task.unit)
|
.use(_ => Task.unit)
|
||||||
.onErrorHandleWith(ex => UIO(ex.printStackTrace()))
|
.onErrorHandleWith(ex => UIO(ex.printStackTrace()))
|
||||||
.as(ExitCode.Success)
|
.as(ExitCode.Success)
|
||||||
|
@ -1,361 +0,0 @@
|
|||||||
package nova.monadic_sfx
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
import scala.util.Random
|
|
||||||
|
|
||||||
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._
|
|
||||||
import nova.monadic_sfx.ui.MyFxApp
|
|
||||||
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.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.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,
|
|
||||||
mediaPlayer: MediaPlayerResource
|
|
||||||
)(implicit logger: Logger[Task]) {
|
|
||||||
|
|
||||||
private lazy val _scene = new Scene {
|
|
||||||
root = new HBox {
|
|
||||||
padding = Insets(20)
|
|
||||||
// style = """| -fx-background-color: rgb(38, 38, 38);
|
|
||||||
// | -fx-text-fill: white;""".stripMargin
|
|
||||||
stylesheets ++= Seq(
|
|
||||||
BootstrapFX.bootstrapFXStylesheet,
|
|
||||||
os.rel / "static" / "css" / "main.css"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy val stage = new PrimaryStage {
|
|
||||||
title = "Simple ScalaFX App"
|
|
||||||
scene = _scene
|
|
||||||
minWidth = 700
|
|
||||||
minHeight = 520
|
|
||||||
width = 1280
|
|
||||||
height = 720
|
|
||||||
// resizable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
(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,
|
|
||||||
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 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)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onAction = () => d.show(root)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} yield mainSceneNode
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestModel(_name: String, _age: Int) {
|
|
||||||
val name = StringProperty(_name).readOnly
|
|
||||||
val age = ObjectProperty(_age).readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
object Test {
|
|
||||||
val items = ObservableBuffer(
|
|
||||||
new TestModel("hmm", 1),
|
|
||||||
new TestModel("hmm2", 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
val ttv = new TableView[TestModel](items) {
|
|
||||||
columns ++= Seq(
|
|
||||||
new TableColumn[TestModel, String] {
|
|
||||||
text = "Name"
|
|
||||||
cellValueFactory = { _.value.name }
|
|
||||||
},
|
|
||||||
new TableColumn[TestModel, Int] {
|
|
||||||
text = "Age"
|
|
||||||
cellValueFactory = { _.value.age }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package nova.monadic_sfx.executors
|
package nova.monadic_sfx.concurrent
|
||||||
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.AbstractExecutorService
|
import java.util.concurrent.AbstractExecutorService
|
45
src/main/scala/nova/monadic_sfx/concurrent/Schedulers.scala
Normal file
45
src/main/scala/nova/monadic_sfx/concurrent/Schedulers.scala
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package nova.monadic_sfx.concurrent
|
||||||
|
|
||||||
|
import com.typesafe.scalalogging.Logger
|
||||||
|
import monix.execution.Scheduler
|
||||||
|
import monix.execution.UncaughtExceptionReporter
|
||||||
|
import monix.execution.schedulers.TracingScheduler
|
||||||
|
import cats.effect.Blocker
|
||||||
|
|
||||||
|
final case class Schedulers(
|
||||||
|
io: Schedulers.IoScheduler,
|
||||||
|
async: Schedulers.AsyncScheduler,
|
||||||
|
fx: Schedulers.FxScheduler
|
||||||
|
)
|
||||||
|
|
||||||
|
object Schedulers {
|
||||||
|
val reporter = UncaughtExceptionReporter { ex =>
|
||||||
|
val logger = Logger[Schedulers]
|
||||||
|
logger.error("Uncaught exception", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
val default = Schedulers(
|
||||||
|
IoScheduler(
|
||||||
|
Scheduler
|
||||||
|
.io()
|
||||||
|
.withUncaughtExceptionReporter(Schedulers.reporter)
|
||||||
|
),
|
||||||
|
AsyncScheduler(
|
||||||
|
Scheduler
|
||||||
|
.computation()
|
||||||
|
.withUncaughtExceptionReporter(Schedulers.reporter)
|
||||||
|
),
|
||||||
|
FxScheduler(
|
||||||
|
TracingScheduler(
|
||||||
|
JFXExecutionContexts.fxScheduler
|
||||||
|
.withUncaughtExceptionReporter(Schedulers.reporter)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
final case class AsyncScheduler(value: Scheduler)
|
||||||
|
final case class IoScheduler(value: Scheduler) {
|
||||||
|
val blocker = Blocker.liftExecutionContext(value)
|
||||||
|
}
|
||||||
|
final case class FxScheduler(value: Scheduler)
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
package nova.monadic_sfx.executors
|
|
||||||
|
|
||||||
trait ExecutorsModule {
|
|
||||||
lazy val schedulers = new Schedulers()
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package nova.monadic_sfx.executors
|
|
||||||
|
|
||||||
import com.typesafe.scalalogging.Logger
|
|
||||||
import monix.execution.Scheduler
|
|
||||||
import monix.execution.UncaughtExceptionReporter
|
|
||||||
import monix.execution.schedulers.TracingScheduler
|
|
||||||
|
|
||||||
class Schedulers(
|
|
||||||
val blocking: Scheduler = TracingScheduler(
|
|
||||||
Scheduler
|
|
||||||
.io()
|
|
||||||
.withUncaughtExceptionReporter(Schedulers.reporter)
|
|
||||||
),
|
|
||||||
val async: Scheduler = Scheduler.traced
|
|
||||||
.withUncaughtExceptionReporter(Schedulers.reporter),
|
|
||||||
val fx: Scheduler = TracingScheduler(
|
|
||||||
JFXExecutionContexts.fxScheduler
|
|
||||||
.withUncaughtExceptionReporter(Schedulers.reporter)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
object Schedulers {
|
|
||||||
val reporter = UncaughtExceptionReporter { ex =>
|
|
||||||
val logger = Logger[Schedulers]
|
|
||||||
logger.error("Uncaught exception", ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -27,6 +27,7 @@ import scalafx.event.subscriptions.Subscription
|
|||||||
import scalafx.scene.Scene
|
import scalafx.scene.Scene
|
||||||
import scalafx.scene.control.ButtonBase
|
import scalafx.scene.control.ButtonBase
|
||||||
import scalafx.scene.control.MenuItem
|
import scalafx.scene.control.MenuItem
|
||||||
|
import nova.monadic_sfx.util.reactive.store.Sink2
|
||||||
|
|
||||||
trait JavaFXMonixObservables {
|
trait JavaFXMonixObservables {
|
||||||
import JavaFXMonixObservables._
|
import JavaFXMonixObservables._
|
||||||
@ -106,9 +107,9 @@ object JavaFXMonixObservables {
|
|||||||
|
|
||||||
final class PropertyExt[T, J](private val prop: Property[T, J])
|
final class PropertyExt[T, J](private val prop: Property[T, J])
|
||||||
extends AnyVal {
|
extends AnyVal {
|
||||||
def -->[J1 >: J](sub: Observer[J1]) = {
|
// def -->[J1 >: J](sub: Observer[J1]) = {
|
||||||
prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
// prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
||||||
}
|
// }
|
||||||
|
|
||||||
def ==>(op: Property[T, J]) = {
|
def ==>(op: Property[T, J]) = {
|
||||||
op <== prop
|
op <== prop
|
||||||
@ -141,18 +142,24 @@ object JavaFXMonixObservables {
|
|||||||
final class ObjectPropertyExt[A](private val prop: ObjectProperty[A])
|
final class ObjectPropertyExt[A](private val prop: ObjectProperty[A])
|
||||||
extends AnyVal {
|
extends AnyVal {
|
||||||
|
|
||||||
def -->(sub: Observer[A]) =
|
// def -->(sub: Observer[A]) =
|
||||||
prop.onChange((a, b, c) =>
|
// prop.onChange((a, b, c) =>
|
||||||
if (c != null)
|
// if (c != null)
|
||||||
if (sub.onNext(c) == Ack.Stop) throw new Exception("boom")
|
// if (sub.onNext(c) == Ack.Stop) throw new Exception("boom")
|
||||||
)
|
// )
|
||||||
|
|
||||||
def ==>(op: Property[A, A]) = {
|
def -->(sink: Sink2[A])(implicit s: Scheduler, c: CompositeCancelable) =
|
||||||
prop.onChange((a, b, c) => if (c != null) op() = c)
|
c += observableChange.doOnNextF(sink.offer).subscribe()
|
||||||
|
|
||||||
|
def ==>(op: Property[A, A])(implicit c: CompositeCancelable) = {
|
||||||
|
val canc = prop.onChange((a, b, c) => if (c != null) op() = c)
|
||||||
|
c += Cancelable(() => canc.cancel())
|
||||||
}
|
}
|
||||||
|
|
||||||
def <--(obs: Observable[A])(implicit s: Scheduler) = {
|
def <--(
|
||||||
obs.doOnNextF(v => Coeval(prop() = v)).subscribe()
|
obs: Observable[A]
|
||||||
|
)(implicit s: Scheduler, c: CompositeCancelable) = {
|
||||||
|
c += obs.doOnNextF(v => Coeval(prop() = v)).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
def observableChange[J1 >: A]: Observable[J1] = {
|
def observableChange[J1 >: A]: Observable[J1] = {
|
||||||
@ -170,9 +177,8 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ObservableListExt[A](
|
final class ObservableListExt[A](private val buffer: ObservableBuffer[A])
|
||||||
private val buffer: ObservableBuffer[A]
|
extends AnyVal {
|
||||||
) extends AnyVal {
|
|
||||||
|
|
||||||
// def -->(sub: Observer[A]) =
|
// def -->(sub: Observer[A]) =
|
||||||
// buffer.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
// buffer.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
||||||
@ -181,8 +187,10 @@ object JavaFXMonixObservables {
|
|||||||
// buffer.onChange((a, b, c) => if (c != null) op() = c)
|
// buffer.onChange((a, b, c) => if (c != null) op() = c)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
def <--(obs: Observable[A])(implicit s: Scheduler) = {
|
def <--(
|
||||||
obs
|
obs: Observable[A]
|
||||||
|
)(implicit s: Scheduler, c: CompositeCancelable) = {
|
||||||
|
c += obs
|
||||||
.doOnNextF(v =>
|
.doOnNextF(v =>
|
||||||
for {
|
for {
|
||||||
_ <- Coeval(buffer.clear())
|
_ <- Coeval(buffer.clear())
|
||||||
@ -192,35 +200,35 @@ object JavaFXMonixObservables {
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
def observableChange[J1 >: A]: Observable[J1] = {
|
// def observableChange[J1 >: A]: Observable[J1] = {
|
||||||
import monix.execution.cancelables.SingleAssignCancelable
|
// import monix.execution.cancelables.SingleAssignCancelable
|
||||||
Observable.create(OverflowStrategy.Unbounded) { sub =>
|
// Observable.create(OverflowStrategy.Unbounded) { sub =>
|
||||||
val c = SingleAssignCancelable()
|
// val c = SingleAssignCancelable()
|
||||||
|
|
||||||
implicit val s = sub.scheduler
|
// implicit val s = sub.scheduler
|
||||||
|
|
||||||
val canc =
|
// val canc =
|
||||||
buffer.onChange((buf, _) =>
|
// buffer.onChange((buf, _) =>
|
||||||
loop(sub, buf.toIterable.iterator, c).runToFuture
|
// loop(sub, buf.toIterable.iterator, c).runToFuture
|
||||||
)
|
// )
|
||||||
|
|
||||||
c := Cancelable(() => canc.cancel())
|
// c := Cancelable(() => canc.cancel())
|
||||||
c
|
// c
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private def loop(
|
// private def loop(
|
||||||
sub: Observer[A],
|
// sub: Observer[A],
|
||||||
it: Iterator[A],
|
// it: Iterator[A],
|
||||||
c: Cancelable
|
// c: Cancelable
|
||||||
): Task[Unit] =
|
// ): Task[Unit] =
|
||||||
if (it.hasNext) {
|
// if (it.hasNext) {
|
||||||
val next = it.next()
|
// val next = it.next()
|
||||||
Task.deferFuture(sub.onNext(next)).flatMap {
|
// Task.deferFuture(sub.onNext(next)).flatMap {
|
||||||
case Ack.Continue => loop(sub, it, c)
|
// case Ack.Continue => loop(sub, it, c)
|
||||||
case Ack.Stop => Task(c.cancel())
|
// case Ack.Stop => Task(c.cancel())
|
||||||
}
|
// }
|
||||||
} else Task.unit
|
// } else Task.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StringObservableListExt(
|
final class StringObservableListExt(
|
||||||
@ -235,9 +243,9 @@ object JavaFXMonixObservables {
|
|||||||
final class ReadOnlyPropertyExt[T, J](
|
final class ReadOnlyPropertyExt[T, J](
|
||||||
private val prop: ReadOnlyProperty[T, J]
|
private val prop: ReadOnlyProperty[T, J]
|
||||||
) extends AnyVal {
|
) extends AnyVal {
|
||||||
def -->[J1 >: J](sub: Observer[J1]) = {
|
// def -->[J1 >: J](sub: Observer[J1]) = {
|
||||||
prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
// prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
|
||||||
}
|
// }
|
||||||
|
|
||||||
def ==>(op: Property[T, J]) = {
|
def ==>(op: Property[T, J]) = {
|
||||||
op <== prop
|
op <== prop
|
||||||
@ -261,45 +269,50 @@ object JavaFXMonixObservables {
|
|||||||
final class ObjectPropertyObservableListExt[A](
|
final class ObjectPropertyObservableListExt[A](
|
||||||
private val prop: ObjectProperty[ObservableList[A]]
|
private val prop: ObjectProperty[ObservableList[A]]
|
||||||
) extends AnyVal {
|
) extends AnyVal {
|
||||||
def <--(obs: Observable[Seq[A]])(implicit s: Scheduler) = {
|
|
||||||
obs.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v))).subscribe()
|
def <--(
|
||||||
|
obs: Observable[Seq[A]]
|
||||||
|
)(implicit s: Scheduler, c: CompositeCancelable) = {
|
||||||
|
c += obs
|
||||||
|
.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v)))
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
def -->(sub: Observer[A])(implicit s: Scheduler) = {
|
// def -->(sub: Observer[A])(implicit s: Scheduler) = {
|
||||||
val c = SingleAssignCancelable()
|
// val c = SingleAssignCancelable()
|
||||||
val subs: Subscription = prop.onChange((a, b, c1) =>
|
// val subs: Subscription = prop.onChange((a, b, c1) =>
|
||||||
if (c1 != null)
|
// if (c1 != null)
|
||||||
Iterant[Task]
|
// Iterant[Task]
|
||||||
.fromIterable(c1.toIterable)
|
// .fromIterable(c1.toIterable)
|
||||||
.consume
|
// .consume
|
||||||
.use(consume(sub, c, _))
|
// .use(consume(sub, c, _))
|
||||||
.runToFuture
|
// .runToFuture
|
||||||
)
|
// )
|
||||||
c := Cancelable(() => subs.cancel())
|
// c := Cancelable(() => subs.cancel())
|
||||||
}
|
// }
|
||||||
|
|
||||||
private def loop(sub: Observer[A], it: Iterator[A]): Task[Unit] =
|
// private def loop(sub: Observer[A], it: Iterator[A]): Task[Unit] =
|
||||||
if (it.hasNext) {
|
// if (it.hasNext) {
|
||||||
val next = it.next()
|
// val next = it.next()
|
||||||
Task.deferFuture(sub.onNext(next)).flatMap {
|
// Task.deferFuture(sub.onNext(next)).flatMap {
|
||||||
case Ack.Continue => loop(sub, it)
|
// case Ack.Continue => loop(sub, it)
|
||||||
case Ack.Stop => Task.unit
|
// case Ack.Stop => Task.unit
|
||||||
}
|
// }
|
||||||
} else Task.unit
|
// } else Task.unit
|
||||||
|
|
||||||
private def consume(
|
// private def consume(
|
||||||
sub: Observer[A],
|
// sub: Observer[A],
|
||||||
c: Cancelable,
|
// c: Cancelable,
|
||||||
consumer: Iterant.Consumer[Task, A]
|
// consumer: Iterant.Consumer[Task, A]
|
||||||
): Task[Unit] =
|
// ): Task[Unit] =
|
||||||
consumer.pull.flatMap {
|
// consumer.pull.flatMap {
|
||||||
case Left(value) => Task.unit
|
// case Left(value) => Task.unit
|
||||||
case Right(value) =>
|
// case Right(value) =>
|
||||||
Task.deferFuture(sub.onNext(value)).flatMap {
|
// Task.deferFuture(sub.onNext(value)).flatMap {
|
||||||
case Ack.Continue => consume(sub, c, consumer)
|
// case Ack.Continue => consume(sub, c, consumer)
|
||||||
case Ack.Stop => Task(c.cancel())
|
// case Ack.Stop => Task(c.cancel())
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,23 @@ package nova.monadic_sfx
|
|||||||
|
|
||||||
import monix.eval.TaskLike
|
import monix.eval.TaskLike
|
||||||
import monix.reactive.Observable
|
import monix.reactive.Observable
|
||||||
|
import monix.bio.IO
|
||||||
|
|
||||||
package object implicits
|
package object implicits
|
||||||
extends MySfxObservableImplicits
|
extends MySfxObservableImplicits
|
||||||
with JavaFXMonixObservables {
|
with JavaFXMonixObservables {
|
||||||
|
|
||||||
|
implicit final class MonixEvalTaskExt[T](private val task: monix.eval.Task[T])
|
||||||
|
extends AnyVal {
|
||||||
|
def toIO = IO.deferAction(implicit s => IO.from(task))
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit final class MonixBioTaskExt[T](private val task: monix.bio.Task[T])
|
||||||
|
extends AnyVal {
|
||||||
|
def toTask =
|
||||||
|
monix.eval.Task.deferAction(implicit s => monix.eval.Task.from(task))
|
||||||
|
}
|
||||||
|
|
||||||
implicit class SttpWsOps[F[_]](private val ws: sttp.client.ws.WebSocket[F])
|
implicit class SttpWsOps[F[_]](private val ws: sttp.client.ws.WebSocket[F])
|
||||||
extends AnyVal {
|
extends AnyVal {
|
||||||
def observableSource(implicit F: TaskLike[F]) =
|
def observableSource(implicit F: TaskLike[F]) =
|
||||||
|
@ -10,10 +10,11 @@ import scalafx.scene.layout.VBox
|
|||||||
import scalafx.scene.paint.Color._
|
import scalafx.scene.paint.Color._
|
||||||
import scalafx.scene.paint._
|
import scalafx.scene.paint._
|
||||||
import scalafx.scene.text.Text
|
import scalafx.scene.text.Text
|
||||||
|
import monix.eval.Coeval
|
||||||
|
|
||||||
object DefaultUI {
|
object DefaultScene {
|
||||||
val scene =
|
def apply() =
|
||||||
new Scene {
|
Coeval(new Scene {
|
||||||
fill = Color.rgb(38, 38, 38)
|
fill = Color.rgb(38, 38, 38)
|
||||||
content = new VBox {
|
content = new VBox {
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
@ -48,5 +49,5 @@ object DefaultUI {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
84
src/main/scala/nova/monadic_sfx/ui/FX.scala
Normal file
84
src/main/scala/nova/monadic_sfx/ui/FX.scala
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package nova.monadic_sfx.ui
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
import cats.effect.Resource
|
||||||
|
import io.odin.Logger
|
||||||
|
import monix.bio.Task
|
||||||
|
import monix.execution.CancelablePromise
|
||||||
|
import nova.monadic_sfx.concurrent.Schedulers
|
||||||
|
import nova.monadic_sfx.ui.DefaultScene
|
||||||
|
import scalafx.application.JFXApp3
|
||||||
|
import scalafx.application.JFXApp3.PrimaryStage
|
||||||
|
import monix.bio.UIO
|
||||||
|
import monix.eval.Coeval
|
||||||
|
import scalafx.scene.Scene
|
||||||
|
import scalafx.scene.Parent
|
||||||
|
import scalafx.Includes._
|
||||||
|
import monix.bio.IO
|
||||||
|
import scalafx.scene.Node
|
||||||
|
|
||||||
|
final class FX private (
|
||||||
|
schedulers: Schedulers,
|
||||||
|
delegate: JFXApp3,
|
||||||
|
val await: Task[Unit],
|
||||||
|
val awaitStop: Task[Unit]
|
||||||
|
)(implicit logger: Logger[Task]) {
|
||||||
|
logger.debug("whoopie")
|
||||||
|
|
||||||
|
def runOnFx[E, A](io: IO[E, A]) = io.executeOn(schedulers.fx.value)
|
||||||
|
|
||||||
|
def addToScene(node: UIO[Node]) =
|
||||||
|
runOnFx {
|
||||||
|
for {
|
||||||
|
p <- node
|
||||||
|
_ <- UIO(delegate.stage.scene().getChildren += p)
|
||||||
|
} yield ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object FX {
|
||||||
|
def resource(
|
||||||
|
schedulers: Schedulers,
|
||||||
|
stage: Coeval[PrimaryStage],
|
||||||
|
initialScene: Coeval[Scene] = DefaultScene(),
|
||||||
|
transitionDelay: FiniteDuration = 500.millis
|
||||||
|
)(implicit
|
||||||
|
logger: Logger[Task]
|
||||||
|
): Resource[Task, FX] =
|
||||||
|
Resource
|
||||||
|
.make(for {
|
||||||
|
_ <- logger.info("Starting FX App")
|
||||||
|
makePromise = UIO(CancelablePromise[Unit]())
|
||||||
|
startSignal <- makePromise
|
||||||
|
stopSignal <- makePromise
|
||||||
|
delegate <- Task(new JFXApp3 {
|
||||||
|
def start(): Unit = {
|
||||||
|
stage = new PrimaryStage {
|
||||||
|
scene = initialScene.value()
|
||||||
|
}
|
||||||
|
startSignal.success(())
|
||||||
|
}
|
||||||
|
|
||||||
|
override def stopApp(): Unit = {
|
||||||
|
stopSignal.success(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fib <-
|
||||||
|
Task(delegate.main(Array.empty)).start.executeOn(schedulers.io.value)
|
||||||
|
fxApp <- Task(
|
||||||
|
new FX(
|
||||||
|
schedulers,
|
||||||
|
delegate,
|
||||||
|
fib.join,
|
||||||
|
Task.fromCancelablePromise(stopSignal)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_ <- Task.fromCancelablePromise(startSignal)
|
||||||
|
_ <- Task.sleep(transitionDelay)
|
||||||
|
_ <- Task(delegate.stage = stage.value())
|
||||||
|
.executeOn(schedulers.fx.value)
|
||||||
|
.delayExecution(transitionDelay)
|
||||||
|
} yield fxApp -> fib) { case _ -> fib => fib.cancel }
|
||||||
|
.map { case a -> _ => a }
|
||||||
|
}
|
@ -14,43 +14,42 @@ import scalafx.scene.text.Font
|
|||||||
import cats.effect.Resource
|
import cats.effect.Resource
|
||||||
|
|
||||||
final class FXComponent private (
|
final class FXComponent private (
|
||||||
val rootNode: Parent,
|
val node: Parent,
|
||||||
val cancelable: Cancelable
|
private val cancelable: Cancelable
|
||||||
)
|
) {
|
||||||
|
|
||||||
|
// def toNode(implicit c: CompositeCancelable): Node = {
|
||||||
|
// c += cancelable
|
||||||
|
// rootNode
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
object FXComponent {
|
object FXComponent {
|
||||||
def acquire(f: CompositeCancelable => Task[Parent]) =
|
private def acquire(f: CompositeCancelable => Task[Parent]) =
|
||||||
for {
|
for {
|
||||||
c <- Task(CompositeCancelable())
|
c <- Task(CompositeCancelable())
|
||||||
p <- f(c)
|
p <- f(c)
|
||||||
} yield new FXComponent(p, c)
|
} yield new FXComponent(p, c)
|
||||||
|
|
||||||
def fxComponent2Node(
|
def apply(f: CompositeCancelable => Task[Parent]) =
|
||||||
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()))
|
Resource.make(acquire(f))(comp => Task(comp.cancelable.cancel()))
|
||||||
}
|
}
|
||||||
|
|
||||||
object TestFXComp {
|
object TestFXComp {
|
||||||
val testComp =
|
// val testComp =
|
||||||
FXComponent.resource { implicit c =>
|
// FXComponent.resource { implicit c =>
|
||||||
Task.deferAction { implicit s =>
|
// Task.deferAction { implicit s =>
|
||||||
Task {
|
// Task {
|
||||||
val sub = ConcurrentSubject.publish[jfxc.text.Font]
|
// val sub = ConcurrentSubject.publish[jfxc.text.Font]
|
||||||
val f = ObjectProperty(Font("hmm"))
|
// val f = ObjectProperty(Font("hmm"))
|
||||||
sub.onNext(f())
|
// sub.onNext(f())
|
||||||
new TextField {
|
// new TextField {
|
||||||
font <-- sub
|
// font <-- sub
|
||||||
font <== f
|
// font <== f
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// val x = for {
|
// val x = for {
|
||||||
// comp <- testComp
|
// comp <- testComp
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
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.JFXApp3
|
|
||||||
import scalafx.application.JFXApp3.PrimaryStage
|
|
||||||
|
|
||||||
class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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(delegate.main(Array.empty)).start.executeOn(schedulers.blocking)
|
|
||||||
_ <- Task.fromCancelablePromise(startSignal)
|
|
||||||
_ <- Task.sleep(transitionDelay)
|
|
||||||
_ <- Task(delegate.stage = stage)
|
|
||||||
.executeOn(schedulers.fx)
|
|
||||||
.delayExecution(transitionDelay)
|
|
||||||
} yield Task.fromCancelablePromise(stopSignal) -> fib) {
|
|
||||||
case _ -> fib => fib.cancel
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
package nova.monadic_sfx.ui
|
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
|
|
||||||
import akka.actor.typed._
|
|
||||||
import akka.util.Timeout
|
|
||||||
import com.softwaremill.macwire._
|
|
||||||
import io.odin.Logger
|
|
||||||
import monix.eval.Task
|
|
||||||
import monix.execution.Callback
|
|
||||||
import monix.execution.Scheduler
|
|
||||||
import nova.monadic_sfx.AppTypes
|
|
||||||
import nova.monadic_sfx.actors.Counter
|
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
|
||||||
import nova.monadic_sfx.http.Requesters
|
|
||||||
import scalafx.application.JFXApp
|
|
||||||
|
|
||||||
class MyFxAppOld(
|
|
||||||
logger: Logger[Task],
|
|
||||||
backend: AppTypes.HttpBackend,
|
|
||||||
actorSystem: ActorSystem[SpawnProtocol.Command],
|
|
||||||
requesters: Requesters,
|
|
||||||
schedulers: Schedulers
|
|
||||||
) extends JFXApp {
|
|
||||||
|
|
||||||
implicit lazy val defaultScheduler: Scheduler = schedulers.fx
|
|
||||||
|
|
||||||
// lazy val fxActor: Task[ActorRef[Counter.Command]] = wireWith(
|
|
||||||
// MyFxApp.makeCounterActor _
|
|
||||||
// )
|
|
||||||
|
|
||||||
lazy val application =
|
|
||||||
for {
|
|
||||||
appStage <- Task(wireWith(UiModule.makePrimaryStage _))
|
|
||||||
|
|
||||||
// _ <- Task {
|
|
||||||
// val counterActor = testActor(actorSystem)
|
|
||||||
// counterActor ! (Counter.Increment)
|
|
||||||
// }
|
|
||||||
// ta <- testActor2(actorSystem)
|
|
||||||
// actor <-
|
|
||||||
// actorTask.bracket(actor => Task(actor ! (Counter.Increment)))(actor =>
|
|
||||||
// Task(actor ! (Counter.Stop))
|
|
||||||
// )
|
|
||||||
// actor <- actorTask
|
|
||||||
// actor <- fxActor
|
|
||||||
// _ <- Task(actor ! Counter.Increment)
|
|
||||||
_ <- Task { stage = appStage }
|
|
||||||
_ <- Task.sleep(2.seconds)
|
|
||||||
// loginScene <- wire[LoginScreen].render
|
|
||||||
// _ <- Task {
|
|
||||||
// // appStage.maximized = true
|
|
||||||
// appStage.height = 800
|
|
||||||
// appStage.width = 800
|
|
||||||
// appStage
|
|
||||||
// .scene()
|
|
||||||
// .setRoot(
|
|
||||||
// loginScene
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
} yield ()
|
|
||||||
|
|
||||||
// def testActor(
|
|
||||||
// system: ActorSystem
|
|
||||||
// ): akka.actor.typed.ActorRef[Counter.Command] = {
|
|
||||||
// val behaviour: Behavior[Counter.Command] =
|
|
||||||
// Behaviors.setup(context => wire[Counter])
|
|
||||||
// system.spawn(
|
|
||||||
// behaviour,
|
|
||||||
// "CounterActor",
|
|
||||||
// DispatcherSelector.fromConfig("javafx-dispatcher")
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
application.timed.runAsync(
|
|
||||||
new Callback[Throwable, (FiniteDuration, Unit)] {
|
|
||||||
|
|
||||||
override def onSuccess(value: (FiniteDuration, Unit)): Unit = {
|
|
||||||
val (duration, _) = value
|
|
||||||
println(
|
|
||||||
s"Application started successfully in ${duration.toSeconds} seconds"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def onError(e: Throwable): Unit = {
|
|
||||||
println("Application start failed. Reason -")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
override def stopApp() = {
|
|
||||||
// val stop = for {
|
|
||||||
// actor <- fxActor
|
|
||||||
// _ <- logger.info("Stopping actor counter")
|
|
||||||
// // _ <- Task.fromFuture { actor.ask[Counter.Value](Counter.GetValue) }
|
|
||||||
// t <- Task(actor ! Counter.Stop)
|
|
||||||
// // _ <- Task.sleep(1.second)
|
|
||||||
// _ <- logger.info("Counter actor stopped")
|
|
||||||
// } yield ()
|
|
||||||
// stop.runAsyncAndForget
|
|
||||||
// // Platform.exit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object MyFxAppOld {
|
|
||||||
def makeCounterActor(
|
|
||||||
system: ActorSystem[SpawnProtocol.Command],
|
|
||||||
logger: Logger[Task]
|
|
||||||
): Task[ActorRef[Counter.Command]] = {
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern._
|
|
||||||
import scala.concurrent.ExecutionContext
|
|
||||||
|
|
||||||
implicit val timeout: Timeout = Timeout(3.seconds)
|
|
||||||
implicit val ec: ExecutionContext = system.executionContext
|
|
||||||
implicit val scheduler = system.scheduler
|
|
||||||
Task.fromFuture {
|
|
||||||
system.ask(
|
|
||||||
SpawnProtocol.Spawn(
|
|
||||||
behavior = wireWith(Counter.apply _),
|
|
||||||
name = "counterActor",
|
|
||||||
DispatcherSelector.fromConfig("javafx-dispatcher"),
|
|
||||||
_
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package nova.monadic_sfx.ui
|
|
||||||
|
|
||||||
import akka.actor.typed._
|
|
||||||
import cats.effect.Resource
|
|
||||||
import com.softwaremill.macwire._
|
|
||||||
import io.odin.Logger
|
|
||||||
import monix.eval.Task
|
|
||||||
import nova.monadic_sfx.AppTypes
|
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
|
||||||
import nova.monadic_sfx.http.Requesters
|
|
||||||
import scalafx.application.JFXApp
|
|
||||||
import scalafx.application.JFXApp.PrimaryStage
|
|
||||||
|
|
||||||
trait UiModule {
|
|
||||||
def fxAppResource(
|
|
||||||
logger: Logger[Task],
|
|
||||||
backend: AppTypes.HttpBackend,
|
|
||||||
actorSystem: ActorSystem[SpawnProtocol.Command],
|
|
||||||
requesters: Requesters,
|
|
||||||
schedulers: Schedulers
|
|
||||||
): Resource[Task, JFXApp] =
|
|
||||||
Resource.make(for {
|
|
||||||
_ <- logger.info("Creating FX Application")
|
|
||||||
app <- Task { wire[MyFxAppOld] }
|
|
||||||
} yield (app))(app => logger.info("Stopping FX Application"))
|
|
||||||
}
|
|
||||||
|
|
||||||
object UiModule {
|
|
||||||
def makePrimaryStage(
|
|
||||||
backend: AppTypes.HttpBackend,
|
|
||||||
actorSystem: ActorSystem[SpawnProtocol.Command]
|
|
||||||
) = {
|
|
||||||
new PrimaryStage {
|
|
||||||
scene = DefaultUI.scene
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,6 +21,7 @@ import nova.monadic_sfx.util.reactive.store.Middlewares
|
|||||||
import nova.monadic_sfx.util.reactive.store.Reducer
|
import nova.monadic_sfx.util.reactive.store.Reducer
|
||||||
import nova.monadic_sfx.util.reactive.store.Store
|
import nova.monadic_sfx.util.reactive.store.Store
|
||||||
import scalafx.scene.Parent
|
import scalafx.scene.Parent
|
||||||
|
import nova.monadic_sfx.util.reactive.store.MyStore
|
||||||
|
|
||||||
object FXRouter {
|
object FXRouter {
|
||||||
|
|
||||||
@ -42,9 +43,11 @@ object FXRouter {
|
|||||||
|
|
||||||
type FXStore[P] = Store[Action[P], State[P]]
|
type FXStore[P] = Store[Action[P], State[P]]
|
||||||
|
|
||||||
|
type FXStore2[P] = MyStore[Action[P], State[P]]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
|
final class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
|
||||||
import FXRouter._
|
import FXRouter._
|
||||||
|
|
||||||
def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
|
def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
|
||||||
@ -64,6 +67,24 @@ class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
|
|||||||
} yield store
|
} yield store
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def store2(initialPage: P, logger: Logger[Task]) =
|
||||||
|
Task.deferAction(implicit s =>
|
||||||
|
for {
|
||||||
|
mw <- Middlewares.actionLoggerMiddleware[Action[P], State[P]](
|
||||||
|
logger,
|
||||||
|
"RouterStore"
|
||||||
|
)
|
||||||
|
store <- Store.backpressured[Action[P], State[P]](
|
||||||
|
Replace(initialPage),
|
||||||
|
State(initialPage, History(initialPage)),
|
||||||
|
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _),
|
||||||
|
logger,
|
||||||
|
Seq(mw)
|
||||||
|
// Seq(classOf[HistoryEvent[P]])
|
||||||
|
)
|
||||||
|
} yield store
|
||||||
|
)
|
||||||
|
|
||||||
def reducer(
|
def reducer(
|
||||||
state: State[P],
|
state: State[P],
|
||||||
action: Action[P]
|
action: Action[P]
|
||||||
@ -113,6 +134,35 @@ class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def render2(
|
||||||
|
resolver: P => Parent,
|
||||||
|
transitionDelay: FiniteDuration = 500.millis
|
||||||
|
)(implicit store: FXStore2[P]) =
|
||||||
|
store.source
|
||||||
|
.filter {
|
||||||
|
case (a, _) => a =!= FXRouter.Forward
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
case (a, _) => a =!= FXRouter.Backward
|
||||||
|
}
|
||||||
|
.distinctUntilChanged
|
||||||
|
.flatMap {
|
||||||
|
case (_, FXRouter.State(p, _)) =>
|
||||||
|
Observable.from(Coeval(new JFXSpinner)) ++ Observable.from(
|
||||||
|
IOUtils.toTask(
|
||||||
|
Task
|
||||||
|
.racePair(
|
||||||
|
Task.sleep(transitionDelay),
|
||||||
|
Task(resolver(p))
|
||||||
|
)
|
||||||
|
.flatMap {
|
||||||
|
case Left(_ -> fib) => fib.join
|
||||||
|
case Right(fib -> res) => fib.join >> Task.pure(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def link(
|
def link(
|
||||||
page: P,
|
page: P,
|
||||||
store: FXStore[P]
|
store: FXStore[P]
|
||||||
@ -128,7 +178,7 @@ object Page {
|
|||||||
final case object UserHome extends Page
|
final case object UserHome extends Page
|
||||||
final case object Todo extends Page
|
final case object Todo extends Page
|
||||||
|
|
||||||
implicit val eqForPage = Eq.fromUniversalEquals[Page]
|
implicit val eq = Eq.fromUniversalEquals[Page]
|
||||||
}
|
}
|
||||||
|
|
||||||
// case class State()
|
// case class State()
|
||||||
|
@ -8,6 +8,7 @@ import monix.bio.Task
|
|||||||
import nova.monadic_sfx.util.reactive.store.Middlewares.actionLoggerMiddleware
|
import nova.monadic_sfx.util.reactive.store.Middlewares.actionLoggerMiddleware
|
||||||
import nova.monadic_sfx.util.reactive.store.Reducer
|
import nova.monadic_sfx.util.reactive.store.Reducer
|
||||||
import nova.monadic_sfx.util.reactive.store.Store
|
import nova.monadic_sfx.util.reactive.store.Store
|
||||||
|
import nova.monadic_sfx.util.reactive.store.MyStore
|
||||||
|
|
||||||
case class Todo(id: Int, content: String)
|
case class Todo(id: Int, content: String)
|
||||||
object Todo {
|
object Todo {
|
||||||
@ -68,16 +69,17 @@ object TodoListStore {
|
|||||||
case End => (state, None)
|
case End => (state, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply(logger: Logger[Task]): Task[Store[Action, State]] =
|
def apply(logger: Logger[Task]): Task[MyStore[Action, State]] =
|
||||||
Task.deferAction(implicit s =>
|
Task.deferAction(implicit s =>
|
||||||
for {
|
for {
|
||||||
logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore")
|
logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore")
|
||||||
store <-
|
store <-
|
||||||
Store
|
Store
|
||||||
.createL[Action, State](
|
.backpressured[Action, State](
|
||||||
Init,
|
Init,
|
||||||
State(Vector.empty[Todo], 0),
|
State(Vector.empty[Todo], 0),
|
||||||
Reducer.withOptionalEffects(reducer(logger) _),
|
Reducer.withOptionalEffects(reducer(logger) _),
|
||||||
|
logger,
|
||||||
Seq(logMware)
|
Seq(logMware)
|
||||||
)
|
)
|
||||||
} yield store
|
} yield store
|
||||||
|
@ -28,15 +28,19 @@ import scalafx.scene.layout.BorderPane
|
|||||||
import scalafx.scene.layout.HBox
|
import scalafx.scene.layout.HBox
|
||||||
import scalafx.scene.layout.Priority
|
import scalafx.scene.layout.Priority
|
||||||
import scalafx.scene.paint.Color
|
import scalafx.scene.paint.Color
|
||||||
|
import nova.monadic_sfx.util.controls.ActionObservableDsl
|
||||||
|
import nova.monadic_sfx.ui.FXComponent
|
||||||
|
|
||||||
object TodoListView {
|
object TodoListView {
|
||||||
def apply(
|
def apply(
|
||||||
store: Store[TodoListStore.Action, TodoListStore.State]
|
store: MyStore[TodoListStore.Action, TodoListStore.State]
|
||||||
): Task[Parent] =
|
) =
|
||||||
|
FXComponent(implicit cc =>
|
||||||
Task.deferAction(implicit s =>
|
Task.deferAction(implicit s =>
|
||||||
Task {
|
Task {
|
||||||
implicit val cc = CompositeCancelable()
|
import ActionObservableDsl._
|
||||||
val todos = store
|
// implicit val cc = CompositeCancelable()
|
||||||
|
val todos = store.source
|
||||||
.map { case (_, state) => state.todos }
|
.map { case (_, state) => state.todos }
|
||||||
.distinctUntilChanged
|
.distinctUntilChanged
|
||||||
.map(ObservableBuffer.from)
|
.map(ObservableBuffer.from)
|
||||||
@ -95,11 +99,17 @@ object TodoListView {
|
|||||||
items ++= Seq(
|
items ++= Seq(
|
||||||
new MenuItem {
|
new MenuItem {
|
||||||
text = "Delete"
|
text = "Delete"
|
||||||
obsAction.useIterableEval(_ =>
|
// obsAction.(_ =>
|
||||||
|
// selectedItems
|
||||||
|
// .map(todo => TodoListStore.Delete(todo.id))
|
||||||
|
// .toList
|
||||||
|
// ) --> store.sink
|
||||||
|
|
||||||
|
obsAction.flatMapIterable(_ =>
|
||||||
selectedItems
|
selectedItems
|
||||||
.map(todo => TodoListStore.Delete(todo.id))
|
.map(todo => TodoListStore.Delete(todo.id))
|
||||||
.toList
|
.toList
|
||||||
) --> store
|
) --> store.sink
|
||||||
|
|
||||||
// obsAction.split(
|
// obsAction.split(
|
||||||
// _.useLazyEval(me.Task(TodoListStore.Delete(0))) --> store,
|
// _.useLazyEval(me.Task(TodoListStore.Delete(0))) --> store,
|
||||||
@ -134,9 +144,9 @@ object TodoListView {
|
|||||||
// disable <== _selectedItems.map(_.length > 0)
|
// disable <== _selectedItems.map(_.length > 0)
|
||||||
styleClass = Seq("btn", "btn-primary")
|
styleClass = Seq("btn", "btn-primary")
|
||||||
obsAction
|
obsAction
|
||||||
.useLazyEval(
|
.mapEval(_ =>
|
||||||
me.Task(TodoListStore.Add(_content()))
|
me.Task(TodoListStore.Add(_content()))
|
||||||
) --> store
|
) --> store.sink
|
||||||
},
|
},
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
id = "todoEditButton"
|
id = "todoEditButton"
|
||||||
@ -144,7 +154,7 @@ object TodoListView {
|
|||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
disable <== _selectedItems.map(_.length > 1)
|
disable <== _selectedItems.map(_.length > 1)
|
||||||
styleClass = Seq("btn", "btn-info")
|
styleClass = Seq("btn", "btn-info")
|
||||||
obsAction.useLazyEval(
|
obsAction.mapEval(_ =>
|
||||||
me.Task(
|
me.Task(
|
||||||
TodoListStore.Edit(
|
TodoListStore.Edit(
|
||||||
_selectedItems
|
_selectedItems
|
||||||
@ -153,11 +163,12 @@ object TodoListView {
|
|||||||
_content()
|
_content()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) --> store
|
) --> store.sink
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import akka.actor.typed._
|
|||||||
import io.odin.Logger
|
import io.odin.Logger
|
||||||
import monix.eval.Task
|
import monix.eval.Task
|
||||||
import nova.monadic_sfx.AppTypes
|
import nova.monadic_sfx.AppTypes
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
import nova.monadic_sfx.concurrent.Schedulers
|
||||||
import nova.monadic_sfx.http.Requesters
|
import nova.monadic_sfx.http.Requesters
|
||||||
import nova.monadic_sfx.http.requests.DummyRequest
|
import nova.monadic_sfx.http.requests.DummyRequest
|
||||||
import nova.monadic_sfx.ui.screens.Screen
|
import nova.monadic_sfx.ui.screens.Screen
|
||||||
|
@ -31,7 +31,7 @@ final class MutHistory[T](initValue: T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class History[T] private (val state: History.State[T]) {
|
final case class History[T] private (val state: History.State[T]) {
|
||||||
def current = state.values(state.sp)
|
def current = state.values(state.sp)
|
||||||
def push(v: T) = {
|
def push(v: T) = {
|
||||||
val nextState =
|
val nextState =
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
package nova.monadic_sfx.util
|
|
||||||
|
|
||||||
import monix.bio.Task
|
|
||||||
import monix.bio.UIO
|
|
||||||
import monix.catnap.MVar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronization wrapper for a mutable object
|
|
||||||
*
|
|
||||||
* @param obj the mutable object
|
|
||||||
* @param lock lock for synchronization
|
|
||||||
*/
|
|
||||||
class SynchedObject[A](obj: A, lock: MLock) {
|
|
||||||
|
|
||||||
def modify(f: A => Task[Unit]): Task[Unit] =
|
|
||||||
lock.greenLight(f(obj))
|
|
||||||
|
|
||||||
def get: Task[A] = lock.greenLight(Task(obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
object SynchedObject {
|
|
||||||
def apply[A](obj: A) =
|
|
||||||
MVar[Task]
|
|
||||||
.of(())
|
|
||||||
.map(m => new MLock(m))
|
|
||||||
.flatMap(lock => Task(new SynchedObject(obj, lock)))
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MLock(mvar: MVar[Task, Unit]) {
|
|
||||||
def acquire: Task[Unit] =
|
|
||||||
mvar.take
|
|
||||||
|
|
||||||
def release: Task[Unit] =
|
|
||||||
mvar.put(())
|
|
||||||
|
|
||||||
def greenLight[A](fa: Task[A]): Task[A] =
|
|
||||||
for {
|
|
||||||
_ <- acquire
|
|
||||||
a <- fa.doOnCancel(
|
|
||||||
release.onErrorHandleWith(ex => UIO(println(ex.getMessage())))
|
|
||||||
)
|
|
||||||
_ <- release
|
|
||||||
} yield a
|
|
||||||
}
|
|
@ -6,93 +6,112 @@ import monix.execution.cancelables.CompositeCancelable
|
|||||||
import monix.reactive.Observable
|
import monix.reactive.Observable
|
||||||
import monix.reactive.Observer
|
import monix.reactive.Observer
|
||||||
import monix.{eval => me}
|
import monix.{eval => me}
|
||||||
|
import nova.monadic_sfx.util.reactive.store.Sink2
|
||||||
|
import nova.monadic_sfx.implicits._
|
||||||
|
|
||||||
class ActionObservableExecutor[T](
|
object ActionObservableDsl {
|
||||||
|
implicit final class ActionObservableExecutor[T](
|
||||||
private val delegate: Observable[T]
|
private val delegate: Observable[T]
|
||||||
) extends AnyVal {
|
) {
|
||||||
def -->(sub: Observer[T])(implicit s: Scheduler) =
|
// def -->[G](sub: G)(implicit s: Scheduler, G: Sink[G, T]) =
|
||||||
delegate
|
// // delegate
|
||||||
.doOnNext(el => me.Task.deferFuture(sub.onNext(el)) >> me.Task.unit)
|
// // .doOnNext(el => me.Task.deferFuture(sub.onNext(el)) >> me.Task.unit)
|
||||||
|
// // .subscribe()
|
||||||
|
// delegate
|
||||||
|
// .doOnNext(el => G.offer(sub, el))
|
||||||
|
// .subscribe()
|
||||||
|
|
||||||
|
def -->(
|
||||||
|
sink: Sink2[T]
|
||||||
|
)(implicit s: Scheduler, c: CompositeCancelable) =
|
||||||
|
// delegate
|
||||||
|
// .doOnNext(el => me.Task.deferFuture(sub.onNext(el)) >> me.Task.unit)
|
||||||
|
// .subscribe()
|
||||||
|
c += delegate
|
||||||
|
.doOnNext(el => sink.offer(el).toTask)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|
||||||
}
|
|
||||||
class ActionObservableBuilder[A](
|
|
||||||
private val observableAction: Observable[A]
|
|
||||||
) extends AnyVal {
|
|
||||||
def useLazyEval[T](v: => me.Task[T]) =
|
|
||||||
new ActionObservableExecutor[T](observableAction.mapEval(_ => v))
|
|
||||||
|
|
||||||
def useEval[T](cb: A => me.Task[T]) =
|
|
||||||
new ActionObservableExecutor[T](
|
|
||||||
observableAction.mapEval(cb)
|
|
||||||
)
|
|
||||||
|
|
||||||
def useIterableEval[T](cb: A => collection.immutable.Iterable[T]) =
|
|
||||||
new ActionObservableExecutor[T](
|
|
||||||
observableAction.flatMap(a =>
|
|
||||||
Observable.suspend(Observable.fromIterable(cb(a)))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def doOnNext(cb: A => me.Task[Unit]): ActionObservableBuilder[A] =
|
|
||||||
new ActionObservableBuilder(observableAction.doOnNext(cb))
|
|
||||||
|
|
||||||
def mapEval[B](cb: A => me.Task[B]) =
|
|
||||||
new ActionObservableBuilder(observableAction.mapEval(cb))
|
|
||||||
|
|
||||||
def underlying = observableAction
|
|
||||||
|
|
||||||
// Caution: Experimental stuff below..
|
|
||||||
|
|
||||||
def useEval2[B, C](f: A => me.Task[B], g: A => me.Task[C]) =
|
|
||||||
new ActionObservableExecutor[(B, C)](
|
|
||||||
observableAction.publishSelector(conn =>
|
|
||||||
conn
|
|
||||||
.mapEval(f)
|
|
||||||
.switchMap(b =>
|
|
||||||
conn.mapEval(a =>
|
|
||||||
for {
|
|
||||||
c <- g(a)
|
|
||||||
} yield (b, c)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def bifurcate[B, C](
|
|
||||||
f: ActionObservableBuilder[A] => B,
|
|
||||||
g: ActionObservableBuilder[A] => C
|
|
||||||
)(implicit s: Scheduler) =
|
|
||||||
observableAction
|
|
||||||
.publishSelector(conn =>
|
|
||||||
Observable(
|
|
||||||
Observable.unit.doOnNext(_ =>
|
|
||||||
me.Task(f(new ActionObservableBuilder[A](conn))) >> me.Task.unit
|
|
||||||
),
|
|
||||||
Observable.unit.doOnNext(_ =>
|
|
||||||
me.Task(g(new ActionObservableBuilder[A](conn))) >> me.Task.unit
|
|
||||||
)
|
|
||||||
).merge
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
def split(
|
|
||||||
lst: (ActionObservableBuilder[A] => Cancelable)*
|
|
||||||
)(implicit s: Scheduler): Cancelable = {
|
|
||||||
val comp = CompositeCancelable()
|
|
||||||
comp += observableAction
|
|
||||||
.publishSelector(conn =>
|
|
||||||
Observable(
|
|
||||||
lst.map(f =>
|
|
||||||
Observable.unit.doOnNext(_ =>
|
|
||||||
me.Task(
|
|
||||||
comp += f(new ActionObservableBuilder[A](conn))
|
|
||||||
) >> me.Task.unit
|
|
||||||
)
|
|
||||||
): _*
|
|
||||||
).merge
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// final class ActionObservableBuilder[A](
|
||||||
|
// private val observableAction: Observable[A]
|
||||||
|
// ) extends AnyVal {
|
||||||
|
// def useLazyEval[T](v: me.Task[T]) =
|
||||||
|
// new ActionObservableExecutor[T](observableAction.mapEval(_ => v))
|
||||||
|
|
||||||
|
// def useEval[T](cb: A => me.Task[T]) =
|
||||||
|
// new ActionObservableExecutor[T](
|
||||||
|
// observableAction.mapEval(cb)
|
||||||
|
// )
|
||||||
|
|
||||||
|
// def useIterableEval[T](cb: A => collection.immutable.Iterable[T]) =
|
||||||
|
// new ActionObservableExecutor[T](
|
||||||
|
// observableAction.flatMap(a =>
|
||||||
|
// Observable.suspend(Observable.fromIterable(cb(a)))
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
|
||||||
|
// def use = new ActionObservableExecutor(observableAction)
|
||||||
|
|
||||||
|
// def doOnNext(cb: A => me.Task[Unit]): ActionObservableBuilder[A] =
|
||||||
|
// new ActionObservableBuilder(observableAction.doOnNext(cb))
|
||||||
|
|
||||||
|
// def mapEval[B](cb: A => me.Task[B]) =
|
||||||
|
// new ActionObservableBuilder(observableAction.mapEval(cb))
|
||||||
|
|
||||||
|
// def underlying = observableAction
|
||||||
|
|
||||||
|
// // Caution: Experimental stuff below..
|
||||||
|
|
||||||
|
// def useEval2[B, C](f: A => me.Task[B], g: A => me.Task[C]) =
|
||||||
|
// new ActionObservableExecutor[(B, C)](
|
||||||
|
// observableAction.publishSelector(conn =>
|
||||||
|
// conn
|
||||||
|
// .mapEval(f)
|
||||||
|
// .switchMap(b =>
|
||||||
|
// conn.mapEval(a =>
|
||||||
|
// for {
|
||||||
|
// c <- g(a)
|
||||||
|
// } yield (b, c)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // def bifurcate[B, C](
|
||||||
|
// // f: ActionObservableBuilder[A] => B,
|
||||||
|
// // g: ActionObservableBuilder[A] => C
|
||||||
|
// // )(implicit s: Scheduler) =
|
||||||
|
// // observableAction
|
||||||
|
// // .publishSelector(conn =>
|
||||||
|
// // Observable(
|
||||||
|
// // Observable.unit.doOnNext(_ =>
|
||||||
|
// // me.Task(f(new ActionObservableBuilder[A](conn))) >> me.Task.unit
|
||||||
|
// // ),
|
||||||
|
// // Observable.unit.doOnNext(_ =>
|
||||||
|
// // me.Task(g(new ActionObservableBuilder[A](conn))) >> me.Task.unit
|
||||||
|
// // )
|
||||||
|
// // ).merge
|
||||||
|
// // )
|
||||||
|
// // .subscribe()
|
||||||
|
|
||||||
|
// def split(
|
||||||
|
// lst: (ActionObservableBuilder[A] => Cancelable)*
|
||||||
|
// )(implicit s: Scheduler): Cancelable = {
|
||||||
|
// val cc = CompositeCancelable()
|
||||||
|
// cc += observableAction
|
||||||
|
// .publishSelector(conn =>
|
||||||
|
// Observable(
|
||||||
|
// lst.map(f =>
|
||||||
|
// Observable.unit.doOnNext(_ =>
|
||||||
|
// me.Task(
|
||||||
|
// cc += f(new ActionObservableBuilder[A](conn))
|
||||||
|
// ) >> me.Task.unit
|
||||||
|
// )
|
||||||
|
// ): _*
|
||||||
|
// ).merge
|
||||||
|
// )
|
||||||
|
// .subscribe()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
@ -34,6 +34,6 @@ class JFXButton(
|
|||||||
|
|
||||||
def ripplerFill_=(b: jfxsp.Paint): Unit = ripplerFill() = b
|
def ripplerFill_=(b: jfxsp.Paint): Unit = ripplerFill() = b
|
||||||
|
|
||||||
def obsAction = new ActionObservableBuilder(this.observableAction)
|
def obsAction = this.observableAction
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,5 @@ package nova.monadic_sfx.util.controls
|
|||||||
import nova.monadic_sfx.implicits._
|
import nova.monadic_sfx.implicits._
|
||||||
import scalafx.scene.{control => sfxc}
|
import scalafx.scene.{control => sfxc}
|
||||||
class MenuItem extends sfxc.MenuItem {
|
class MenuItem extends sfxc.MenuItem {
|
||||||
def obsAction = new ActionObservableBuilder(this.observableAction)
|
def obsAction = this.observableAction
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
package nova.monadic_sfx.util.reactive.store
|
||||||
|
|
||||||
|
import monix.eval.Task
|
||||||
|
import monix.catnap.ConcurrentQueue
|
||||||
|
import monix.reactive.Observer
|
||||||
|
|
||||||
|
trait Sink[A, -B] {
|
||||||
|
def offer(inst: A, b: B): Task[Unit]
|
||||||
|
}
|
||||||
|
|
||||||
|
object Sink {
|
||||||
|
implicit def sinkForCq[B] =
|
||||||
|
new Sink[ConcurrentQueue[Task, B], B] {
|
||||||
|
|
||||||
|
override def offer(queue: ConcurrentQueue[Task, B], b: B): Task[Unit] =
|
||||||
|
queue.offer(b)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit def sinkForObserver[B] =
|
||||||
|
new Sink[Observer[B], B] {
|
||||||
|
|
||||||
|
override def offer(inst: Observer[B], b: B): Task[Unit] =
|
||||||
|
Task.deferFuture(inst.onNext(b)).void
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit def sinkForStore[B, C] =
|
||||||
|
new Sink[Store[B, C], B] {
|
||||||
|
|
||||||
|
override def offer(inst: Store[B, C], b: B): Task[Unit] =
|
||||||
|
Task.deferFuture(inst.onNext(b)).void
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// implicitly[Sink[Store[Int, Int], Int]]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Sink2[-B] {
|
||||||
|
def offer(b: B): monix.bio.Task[Unit]
|
||||||
|
}
|
||||||
|
|
||||||
|
object Sink2 {
|
||||||
|
def concurrentQueue[B](queue: ConcurrentQueue[monix.bio.Task, B]): Sink2[B] =
|
||||||
|
new Sink2[B] {
|
||||||
|
def offer(b: B): monix.bio.Task[Unit] = queue.offer(b)
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,13 @@ import monix.eval.Coeval
|
|||||||
import monix.reactive.Observer
|
import monix.reactive.Observer
|
||||||
import monix.reactive.OverflowStrategy
|
import monix.reactive.OverflowStrategy
|
||||||
import monix.reactive.subjects.ConcurrentSubject
|
import monix.reactive.subjects.ConcurrentSubject
|
||||||
|
import cats.effect.Resource
|
||||||
|
import monix.{eval => me}
|
||||||
|
import monix.catnap.ConcurrentQueue
|
||||||
|
import nova.monadic_sfx.util.IOUtils
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import io.odin.Logger
|
||||||
|
import nova.monadic_sfx.implicits._
|
||||||
|
|
||||||
object Store {
|
object Store {
|
||||||
def createL[A, M](
|
def createL[A, M](
|
||||||
@ -54,4 +61,64 @@ object Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// : Resource[Task, Store[A, M]]
|
||||||
|
def backpressured[A, M](
|
||||||
|
initialAction: A,
|
||||||
|
initialState: M,
|
||||||
|
reducer: Reducer[A, M],
|
||||||
|
logger: Logger[Task],
|
||||||
|
middlewares: Seq[Middleware[A, M]] = Seq.empty
|
||||||
|
) = {
|
||||||
|
|
||||||
|
for {
|
||||||
|
queue <- ConcurrentQueue[Task].bounded[A](10)
|
||||||
|
source <- Task.deferAction(implicit s =>
|
||||||
|
Task {
|
||||||
|
val fold: ((A, M), A) => Task[(A, M)] = {
|
||||||
|
case ((_, state), action) =>
|
||||||
|
for {
|
||||||
|
_ <- Task.unit
|
||||||
|
(newState, effects) = reducer(state, action)
|
||||||
|
_ <-
|
||||||
|
effects
|
||||||
|
.doOnNextF(queue.offer)
|
||||||
|
.completedL
|
||||||
|
.toIO
|
||||||
|
// .start
|
||||||
|
|
||||||
|
} yield action -> newState
|
||||||
|
}
|
||||||
|
|
||||||
|
val obs = Observable
|
||||||
|
.repeatEvalF(queue.poll)
|
||||||
|
.scanEval0F[Task, (A, M)](
|
||||||
|
Task.pure(initialAction -> initialState)
|
||||||
|
)(fold)
|
||||||
|
|
||||||
|
val res =
|
||||||
|
// middlewares
|
||||||
|
// .foldLeft(obs) {
|
||||||
|
// case (obs, middleware) => middleware(obs)
|
||||||
|
// }
|
||||||
|
obs
|
||||||
|
.doOnNextF(i => logger.debug(s"Emitted item 1: $i"))
|
||||||
|
.behavior(initialAction -> initialState)
|
||||||
|
.refCount
|
||||||
|
|
||||||
|
// res.subscribe(Observer.empty)
|
||||||
|
|
||||||
|
// .doOnNextF(i => Coeval(println(s"Emitted item 2: $i")))
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} yield new MyStore(Sink2.concurrentQueue(queue), source)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class MyStore[A, M](
|
||||||
|
val sink: Sink2[A],
|
||||||
|
val source: Observable[(A, M)]
|
||||||
|
)
|
||||||
|
@ -15,4 +15,5 @@ package object store {
|
|||||||
* @tparam M The Model Type
|
* @tparam M The Model Type
|
||||||
*/
|
*/
|
||||||
type Reducer[A, M] = (M, A) => (M, Observable[A])
|
type Reducer[A, M] = (M, A) => (M, Observable[A])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
46
src/test/scala/BackpressuredStoreTest.scala
Executable file
46
src/test/scala/BackpressuredStoreTest.scala
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import monix.bio.Task
|
||||||
|
import nova.monadic_sfx.util.reactive.store.Store
|
||||||
|
import nova.monadic_sfx.util.History
|
||||||
|
import nova.monadic_sfx.util.reactive.store.Reducer
|
||||||
|
import nova.monadic_sfx.ui.components.router.Page
|
||||||
|
import nova.monadic_sfx.util.IOUtils.toIO
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import cats.effect.Resource
|
||||||
|
import nova.monadic_sfx.ui.components.router.FXRouter
|
||||||
|
import io.odin.consoleLogger
|
||||||
|
class BackpressuredStoreTest extends AnyFunSuite {
|
||||||
|
import monix.execution.Scheduler.Implicits.global
|
||||||
|
val logger = consoleLogger[Task]()
|
||||||
|
test("backpressed store test") {
|
||||||
|
(for {
|
||||||
|
_ <-
|
||||||
|
Resource
|
||||||
|
.eval(new FXRouter[Page].store2(Page.Home, logger))
|
||||||
|
.use { myStore =>
|
||||||
|
for {
|
||||||
|
_ <- toIO(
|
||||||
|
myStore.source
|
||||||
|
.doOnNextF(_ => Task.sleep(1.seconds))
|
||||||
|
.doOnNextF(item => logger.debug(s"Task1: Got Item $item"))
|
||||||
|
.completedL
|
||||||
|
).startAndForget
|
||||||
|
_ <- toIO(
|
||||||
|
myStore.source
|
||||||
|
.doOnNextF(_ => Task.sleep(3.seconds))
|
||||||
|
.doOnNextF(item => logger.debug(s"Task2: Got Item $item"))
|
||||||
|
.completedL
|
||||||
|
).startAndForget
|
||||||
|
_ <- myStore.sink.offer(FXRouter.Replace(Page.Home))
|
||||||
|
_ <- myStore.sink.offer(FXRouter.Replace(Page.Todo))
|
||||||
|
_ <- myStore.sink.offer(FXRouter.Replace(Page.UserHome))
|
||||||
|
_ <- myStore.sink.offer(FXRouter.Backward)
|
||||||
|
_ <- Task.sleep(25.seconds)
|
||||||
|
} yield ()
|
||||||
|
|
||||||
|
}
|
||||||
|
// _ <- Task.sleep(2.seconds)
|
||||||
|
} yield ()).runSyncUnsafe(26.seconds)
|
||||||
|
}
|
||||||
|
}
|
104
src/test/scala/ObservableTest.scala
Executable file
104
src/test/scala/ObservableTest.scala
Executable file
@ -0,0 +1,104 @@
|
|||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import monix.catnap.ConcurrentQueue
|
||||||
|
import monix.eval.Task
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
class ObservableTest extends AnyFunSuite {
|
||||||
|
import monix.execution.Scheduler.Implicits.global
|
||||||
|
test("observable state machine") {
|
||||||
|
(for {
|
||||||
|
_ <- Task.unit
|
||||||
|
sm <- MonixStateMachine()
|
||||||
|
_ <-
|
||||||
|
Task
|
||||||
|
.parSequence(
|
||||||
|
List(
|
||||||
|
sm.source
|
||||||
|
.doOnNext(item => Task(println(s"Task 1: Got $item")))
|
||||||
|
.completedL,
|
||||||
|
sm.source
|
||||||
|
.doOnNext(item => Task(println(s"Task 2: Got $item")))
|
||||||
|
.completedL,
|
||||||
|
sm.tell(MonixStateMachine.Start) >>
|
||||||
|
Observable
|
||||||
|
// .interval(1.second)
|
||||||
|
.interval(500.millis)
|
||||||
|
.doOnNext(_ => sm.tell(MonixStateMachine.Incr))
|
||||||
|
.takeUntil(Observable.unit.delayExecution(5.seconds))
|
||||||
|
.completedL >>
|
||||||
|
sm.tell(MonixStateMachine.Stop) >>
|
||||||
|
sm.tell(MonixStateMachine.Incr)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.void
|
||||||
|
.start
|
||||||
|
.bracket(_ => Task.sleep(8.seconds))(_.cancel)
|
||||||
|
} yield ()).runSyncUnsafe(10.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MonixStateMachine(
|
||||||
|
queue: ConcurrentQueue[Task, MonixStateMachine.Command],
|
||||||
|
val source: Observable[(MonixStateMachine.State, MonixStateMachine.Data)]
|
||||||
|
) {
|
||||||
|
import MonixStateMachine._
|
||||||
|
|
||||||
|
def tell(item: Command) = queue.offer(item)
|
||||||
|
|
||||||
|
}
|
||||||
|
object MonixStateMachine {
|
||||||
|
|
||||||
|
sealed trait State
|
||||||
|
case object Idle extends State
|
||||||
|
case object Active extends State
|
||||||
|
|
||||||
|
sealed trait Command
|
||||||
|
case object Incr extends Command
|
||||||
|
case object Start extends Command
|
||||||
|
case object Stop extends Command
|
||||||
|
|
||||||
|
case class Data(num: Int)
|
||||||
|
|
||||||
|
private def source(queue: ConcurrentQueue[Task, Command]) =
|
||||||
|
Task.deferAction(implicit s =>
|
||||||
|
Task(
|
||||||
|
Observable
|
||||||
|
.repeatEvalF(queue.poll)
|
||||||
|
.scan((Idle: State, Data(0))) {
|
||||||
|
case ((state, data), command) =>
|
||||||
|
state match {
|
||||||
|
case Idle =>
|
||||||
|
println("Entered idle")
|
||||||
|
command match {
|
||||||
|
case Incr =>
|
||||||
|
println("Not active ")
|
||||||
|
(Idle, data)
|
||||||
|
case Start => (Active, data)
|
||||||
|
case Stop =>
|
||||||
|
println("Already stopped")
|
||||||
|
(Idle, data)
|
||||||
|
|
||||||
|
}
|
||||||
|
case Active =>
|
||||||
|
println("Entered Active")
|
||||||
|
command match {
|
||||||
|
case Incr => (Active, data.copy(num = data.num + 1))
|
||||||
|
case Start =>
|
||||||
|
println("Already started")
|
||||||
|
(Active, data)
|
||||||
|
case Stop => (Idle, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.publish
|
||||||
|
.refCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply() =
|
||||||
|
for {
|
||||||
|
queue <- ConcurrentQueue.bounded[Task, Command](10)
|
||||||
|
source <- source(queue)
|
||||||
|
} yield new MonixStateMachine(queue, source)
|
||||||
|
}
|
@ -9,6 +9,7 @@ import sttp.model.ws.WebSocketFrame
|
|||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import nova.monadic_sfx.implicits._
|
import nova.monadic_sfx.implicits._
|
||||||
import monix.catnap.ConcurrentQueue
|
import monix.catnap.ConcurrentQueue
|
||||||
|
import cats.syntax.all._
|
||||||
class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll {
|
class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
implicit val sched = Scheduler.global
|
implicit val sched = Scheduler.global
|
||||||
implicit val backend = HttpClientMonixBackend().runSyncUnsafe()
|
implicit val backend = HttpClientMonixBackend().runSyncUnsafe()
|
||||||
@ -18,6 +19,11 @@ class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll {
|
|||||||
.map(_.result)
|
.map(_.result)
|
||||||
.runSyncUnsafe()
|
.runSyncUnsafe()
|
||||||
|
|
||||||
|
override def afterAll() = {
|
||||||
|
ws.close.runSyncUnsafe()
|
||||||
|
backend.close()
|
||||||
|
}
|
||||||
|
|
||||||
test("open websocket") {
|
test("open websocket") {
|
||||||
(for {
|
(for {
|
||||||
isOpen <- ws.isOpen
|
isOpen <- ws.isOpen
|
||||||
@ -39,8 +45,8 @@ class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll {
|
|||||||
queue <- ConcurrentQueue.bounded[Task, Int](10)
|
queue <- ConcurrentQueue.bounded[Task, Int](10)
|
||||||
_ <-
|
_ <-
|
||||||
ws.observableSource
|
ws.observableSource
|
||||||
.filter(_.isRight)
|
.map(_.toOption)
|
||||||
.map(_.right.get)
|
.mapFilter(identity)
|
||||||
.doOnNext(s =>
|
.doOnNext(s =>
|
||||||
Task(println(s"Received item: $s")) >>
|
Task(println(s"Received item: $s")) >>
|
||||||
s.toIntOption.fold(Task.unit)(queue.offer)
|
s.toIntOption.fold(Task.unit)(queue.offer)
|
||||||
@ -53,9 +59,4 @@ class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll {
|
|||||||
_ <- Task(assert(items != Seq(1, 2, 3, 4, 5, 6)))
|
_ <- Task(assert(items != Seq(1, 2, 3, 4, 5, 6)))
|
||||||
} yield ()).runSyncUnsafe(10.seconds)
|
} yield ()).runSyncUnsafe(10.seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def afterAll() = {
|
|
||||||
ws.close.runSyncUnsafe()
|
|
||||||
backend.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user