diff --git a/.gitignore b/.gitignore index 9054cba..73ea9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ metals.sbt /project/project .bsp +.attach_pid** diff --git a/build.sbt b/build.sbt index 0bfecb1..07edf91 100644 --- a/build.sbt +++ b/build.sbt @@ -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( diff --git a/project/build.properties b/project/build.properties index 9061e4e..474af01 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ -sbt.version=1.4.3 +sbt.version=1.4.5 diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index 802cb93..e68d0ca 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -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; -} - diff --git a/src/main/scala/nova/monadic_sfx/Main.scala b/src/main/scala/nova/monadic_sfx/Main.scala index 15282bc..de643a6 100644 --- a/src/main/scala/nova/monadic_sfx/Main.scala +++ b/src/main/scala/nova/monadic_sfx/Main.scala @@ -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 () diff --git a/src/main/scala/nova/monadic_sfx/MainApp.scala b/src/main/scala/nova/monadic_sfx/MainApp.scala index 66d3521..b1813e2 100644 --- a/src/main/scala/nova/monadic_sfx/MainApp.scala +++ b/src/main/scala/nova/monadic_sfx/MainApp.scala @@ -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) { diff --git a/src/main/scala/nova/monadic_sfx/MainModule.scala b/src/main/scala/nova/monadic_sfx/MainModule.scala index 7e5deb6..b693d92 100644 --- a/src/main/scala/nova/monadic_sfx/MainModule.scala +++ b/src/main/scala/nova/monadic_sfx/MainModule.scala @@ -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) } diff --git a/src/main/scala/nova/monadic_sfx/Types.scala b/src/main/scala/nova/monadic_sfx/Types.scala index f6f5182..cfd854e 100644 --- a/src/main/scala/nova/monadic_sfx/Types.scala +++ b/src/main/scala/nova/monadic_sfx/Types.scala @@ -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 {} diff --git a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala index 9f9e482..efc3896 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala @@ -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]) = diff --git a/src/main/scala/nova/monadic_sfx/implicits/package.scala b/src/main/scala/nova/monadic_sfx/implicits/package.scala index dff6712..2cda7d2 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/package.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/package.scala @@ -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[_]]( diff --git a/src/main/scala/nova/monadic_sfx/ui/FXComponent.scala b/src/main/scala/nova/monadic_sfx/ui/FXComponent.scala new file mode 100644 index 0000000..825969c --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/ui/FXComponent.scala @@ -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 +} diff --git a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala index 74e245f..24a58cb 100644 --- a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala +++ b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala @@ -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 + } } diff --git a/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala index f7d794e..82a395a 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala @@ -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() diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala index a6b8259..a2b92c1 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala @@ -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, diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala index 5e7425f..b7ed727 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala @@ -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 ) } diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala index d823b5a..476ab00 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala @@ -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 def selectedItems = selectionModel().selectedItems.view - styleClass ++= Seq("text-white") + styleClass ++= Seq("text-white", "clear-list-view") selectionModel().selectionMode = SelectionMode.Multiple selectionModel().selectedItems.observableSeqValue ==> _selectedItems - cc += items <-- todos + items <-- todos.map(_.delegate) val emptyCell = ObjectProperty(new HBox) cellFactory = _ => @@ -56,12 +65,16 @@ object TodoListView { val _text = StringProperty("") val _graphic = ObjectProperty( new HBox { + styleClass ++= Seq("text-white", "strong", "todo-cell") children = Seq( new FontIcon { - iconSize = 10 + iconSize = 20 iconLiteral = IconLiteral.Gmi10k + fill = Color.White }, - new Text { + new Label { + style = "-fx-text-fill: white " + styleClass ++= Seq("text-white", "strong") text <== _text } ) @@ -100,6 +113,7 @@ object TodoListView { ) } }) + } bottom = new HBox { @@ -107,22 +121,29 @@ object TodoListView { padding = Insets(5) children = Seq( new JFXTextField { + id = "todoInputField" + style = "-fx-background-color: rgb(38,38,38);" + styleClass += "text-white" text ==> _content + vgrow = Priority.Always }, new JFXButton { + id = "todoAddButton" text = "Add" alignment = Pos.Center // disable <== _selectedItems.map(_.length > 0) styleClass = Seq("btn", "btn-primary") obsAction - .useLazyEval(me.Task(TodoListStore.Add(_content()))) --> store + .useLazyEval( + me.Task(TodoListStore.Add(_content())) + ) --> store }, new JFXButton { + id = "todoEditButton" text = "Edit" alignment = Pos.Center disable <== _selectedItems.map(_.length > 1) styleClass = Seq("btn", "btn-info") - style = "" obsAction.useLazyEval( me.Task( TodoListStore.Edit( diff --git a/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala b/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala index 300b3b8..1d3f4f7 100644 --- a/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala +++ b/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala @@ -6,13 +6,13 @@ import animatefx.animation.FadeIn import animatefx.util.{SequentialAnimationFX => SeqFX} import cats.effect.Sync import monix.eval.Task +import nova.monadic_sfx.ui.components.todo.TodoListComponentOld 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.JFXTextArea import nova.monadic_sfx.util.controls.JFXTextField -import nova.monadic_sfx.ui.components.todo.TodoListComponentOld import scalafx.collections.ObservableBuffer import scalafx.scene.control.Label import scalafx.scene.layout.HBox diff --git a/src/main/scala/nova/monadic_sfx/util/BoundedStack.scala b/src/main/scala/nova/monadic_sfx/util/BoundedStack.scala new file mode 100644 index 0000000..edff506 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/BoundedStack.scala @@ -0,0 +1,37 @@ +package nova.monadic_sfx.util + +final class BoundedStack private ( + val underlying: List[Int], + val maxSize: Int, + val sp: Int +) { + def push(v: Int): Either[String, BoundedStack] = + if (sp < maxSize) Right(new BoundedStack(v :: underlying, maxSize, sp + 1)) + else Left("overflow") + + def pushAll(v: List[Int]) = + if (sp + v.length < maxSize) + Right(new BoundedStack(v ::: underlying, maxSize, sp + v.length)) + else Left("overflow") + + def pop = + if (sp == 0) Left("Underflow") + else + Right( + underlying(sp) -> new BoundedStack( + underlying.splitAt(sp)._1, + maxSize, + sp - 1 + ) + ) +} + +object BoundedStack { + def apply( + // defaultValues: List[Int] = List.empty, + maxSize: Int = 10 + // sp: Int = 0 + ) = + new BoundedStack(List.empty, maxSize, 0) + +} diff --git a/src/main/scala/nova/monadic_sfx/util/CssPath.scala b/src/main/scala/nova/monadic_sfx/util/CssPath.scala new file mode 100644 index 0000000..5ef17fd --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/CssPath.scala @@ -0,0 +1,18 @@ +package nova.monadic_sfx.util + +import os.RelPath + +trait CssPath[T] { + def path(self: T): String +} + +object CssPath { + implicit val cssPathForString = new CssPath[String] { + def path(self: String): String = self + } + implicit val cssPathForOsRelPath = new CssPath[os.RelPath] { + def path(self: RelPath): String = self.toString() + } + + implicit def any2CssPath[T](t: T)(implicit C: CssPath[T]): CssPath[T] = C +} diff --git a/src/main/scala/nova/monadic_sfx/util/History.scala b/src/main/scala/nova/monadic_sfx/util/History.scala index 9f78b92..270f9f6 100644 --- a/src/main/scala/nova/monadic_sfx/util/History.scala +++ b/src/main/scala/nova/monadic_sfx/util/History.scala @@ -1,6 +1,7 @@ package nova.monadic_sfx.util +import com.softwaremill.quicklens._ -class MutHistory[T](initValue: T) { +final class MutHistory[T](initValue: T) { private var _values = Vector(initValue) def values = _values private var _sp = 0 @@ -29,3 +30,50 @@ class MutHistory[T](initValue: T) { } } } + +final class History[T] private (val state: History.State[T]) { + def current = state.values(state.sp) + def push(v: T) = { + val nextState = + // state.copy(state.values.splitAt(state.sp)._1 :+ v, state.sp + 1) + if (state.sp < state.values.length - 1) { + state.modify(_.values).using(_.splitAt(state.sp)._1 :+ v) + } else { + val s1 = state + .modify(_.values) + .using(_ :+ v) + s1.modify(_.sp) + .setTo(s1.values.length - 1) + } + new History(nextState) + } + def forward = { + // val nextState = + if (!state.values.isEmpty && state.sp < state.values.length - 1) + new History(state.modify(_.sp).using(_ + 1)) + else this + // new History(nextState) + } + def backward = { + // val nextState = + if (state.sp > 0) new History(state.modify(_.sp).using(_ - 1)) else this + // new History(nextState) + } +} + +// final class HistoryImpl() + +object History { + case class State[T](values: Vector[T], sp: Int) + + def apply[T](intialValue: T) = + new History(State(Vector(intialValue), 0)) + +// val history = new History(History.State(Vector.empty, 0)) + + implicit class HistoryOps[T](private val h: History[T]) extends AnyVal { + def :+(v: T) = h.push(v) + } + +} +// History.history.push(1).forward diff --git a/src/main/scala/nova/monadic_sfx/util/MediaPlayerResource.scala b/src/main/scala/nova/monadic_sfx/util/MediaPlayerResource.scala new file mode 100644 index 0000000..053ab30 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/MediaPlayerResource.scala @@ -0,0 +1,31 @@ +package nova.monadic_sfx.util + +import cats.effect.Resource +import monix.bio.Task +import uk.co.caprica.vlcj.factory.MediaPlayerFactory +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer + +class MediaPlayerResource( + val factory: MediaPlayerFactory, + val controller: EmbeddedMediaPlayer +) + +object MediaPlayerResource { + val mediaPlayerFactoryResource = + Resource.make(Task(new MediaPlayerFactory()))(factory => + Task(factory.release()) + ) + + def mediaPlayerControllerResource(factory: MediaPlayerFactory) = + Resource.make(Task { + val players = factory.mediaPlayers() + players.newEmbeddedMediaPlayer() + })(controller => Task(controller.release())) + + def apply() = + for { + factory <- mediaPlayerFactoryResource + controller <- mediaPlayerControllerResource(factory) + } yield new MediaPlayerResource(factory, controller) + +} diff --git a/src/main/scala/nova/monadic_sfx/util/WebSocket.scala b/src/main/scala/nova/monadic_sfx/util/WebSocket.scala new file mode 100644 index 0000000..ac233f7 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/WebSocket.scala @@ -0,0 +1,47 @@ +package nova.monadic_sfx.util + +import monix.bio.Task +import nova.monadic_sfx.AppTypes +import nova.monadic_sfx.implicits._ +import sttp.client._ +import sttp.client.httpclient.monix.MonixWebSocketHandler + +// class WebSocket()(implicit backend: AppTypes.HttpBackend) { + +// // val source = for { + +// // } yield () +// } +object WebSocket { + + def apply()(implicit backend: AppTypes.HttpBackend) = { + Task + .deferAction(implicit s => + IOUtils.toIO( + basicRequest.get(uri"").openWebsocketF(MonixWebSocketHandler()) + ) + ) + .flatMap { r => + val ws = r.result + // val source = Observable.repeatEvalF(ws.receive) + val source2 = ws.observableSource + // ws.send() + val source3 = source2.map { + case Left(value) => () + case Right(value) => value + } + // ws.send() + // val sink = new Observer[WebSocketFrame] { + + // override def onNext(elem: WebSocketFrame): Future[Ack] = ws.send(elem) + + // override def onError(ex: Throwable): Unit = ex.printStackTrace() + + // override def onComplete(): Unit = ws.close + + // } + Task.unit + } + } + +} diff --git a/src/main/scala/nova/monadic_sfx/util/controls/EmbeddedMediaPlayer.scala b/src/main/scala/nova/monadic_sfx/util/controls/EmbeddedMediaPlayer.scala new file mode 100644 index 0000000..fd1f6d2 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/controls/EmbeddedMediaPlayer.scala @@ -0,0 +1,3 @@ +package nova.monadic_sfx.util.controls + + diff --git a/src/main/scala/nova/monadic_sfx/util/controls/JFXButton.scala b/src/main/scala/nova/monadic_sfx/util/controls/JFXButton.scala index 74089ca..952c6e2 100644 --- a/src/main/scala/nova/monadic_sfx/util/controls/JFXButton.scala +++ b/src/main/scala/nova/monadic_sfx/util/controls/JFXButton.scala @@ -2,11 +2,11 @@ package nova.monadic_sfx.util.controls import com.jfoenix.{controls => jfoenixc} import javafx.{scene => jfxs} +import nova.monadic_sfx.implicits._ import scalafx.Includes._ import scalafx.beans.property.ObjectProperty import scalafx.scene.Node import scalafx.scene.control.Button -import nova.monadic_sfx.implicits._ import jfxs.{paint => jfxsp} @@ -30,14 +30,10 @@ class JFXButton( def this(text: String, graphic: Node) = this(new jfoenixc.JFXButton(text, graphic)) - def ripplerFill: ObjectProperty[jfxsp.Paint] = - jfxObjectProperty2sfx(delegate.ripplerFillProperty) + def ripplerFill: ObjectProperty[jfxsp.Paint] = delegate.ripplerFillProperty - def ripplerFill_=(b: jfxsp.Paint): Unit = { - ripplerFill() = b - } + def ripplerFill_=(b: jfxsp.Paint): Unit = ripplerFill() = b - def obsAction = - new ActionObservableBuilder(this.observableAction) + def obsAction = new ActionObservableBuilder(this.observableAction) } diff --git a/src/main/scala/nova/monadic_sfx/util/controls/JFXDialog.scala b/src/main/scala/nova/monadic_sfx/util/controls/JFXDialog.scala new file mode 100644 index 0000000..8718dad --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/controls/JFXDialog.scala @@ -0,0 +1,22 @@ +package nova.monadic_sfx.util.controls + +import com.jfoenix.{controls => jfoenixc} +import scalafx.scene.layout.Region +import scalafx.scene.layout.StackPane + +class JFXDialog( + override val delegate: jfoenixc.JFXDialog = new jfoenixc.JFXDialog +) extends StackPane(delegate) { + def show() = delegate.show() + def show(sp: StackPane) = delegate.show(sp) + def content = delegate.getContent() + def content_=(r: Region) = delegate.setContent(r) + def overlayClose = delegate.overlayCloseProperty() + def overlayClose_=(v: Boolean) = delegate.setOverlayClose(v) + def cacheContainer = delegate.cacheContainerProperty() + def cacheContainer_=(v: Boolean) = delegate.setCacheContainer(v) +} + +object JFXDialog { + implicit def sfxJfXDialog2Jfx(v: JFXDialog): jfoenixc.JFXDialog = v.delegate +} diff --git a/src/main/scala/nova/monadic_sfx/util/controls/JFXListCell.scala b/src/main/scala/nova/monadic_sfx/util/controls/JFXListCell.scala index 42d7413..b259326 100644 --- a/src/main/scala/nova/monadic_sfx/util/controls/JFXListCell.scala +++ b/src/main/scala/nova/monadic_sfx/util/controls/JFXListCell.scala @@ -6,17 +6,34 @@ import scalafx.Includes._ import scalafx.beans.property.ReadOnlyObjectProperty import scalafx.delegate.SFXDelegate import scalafx.scene.control.IndexedCell +import scalafx.scene.control.ListCell import scalafx.scene.control.ListView object JFXListCell { implicit def sfxListCell2jfx[T]( l: JFXListCell[T] - ): jfoenixc.JFXListCell[T] = + ): ListCell[T] = if (l != null) l.delegate else null } class JFXListCell[T]( - override val delegate: jfoenixc.JFXListCell[T] = new jfoenixc.JFXListCell[T] + override val delegate: jfoenixc.JFXListCell[T] = + new jfoenixc.JFXListCell[T] { + override def updateItem( + item: T, + empty: Boolean + ): Unit = { + super.updateItem(item, empty) + // setText(null) + setText(getText()) + setGraphic(getGraphic()) + // setGraphic(null) + // remove empty (Trailing cells) + // setMouseTransparent(true) + // setStyle("-fx-background-color:TRANSPARENT;") + } + override def makeChildrenTransparent(): Unit = {} + } ) extends IndexedCell(delegate) with SFXDelegate[jfoenixc.JFXListCell[T]] { @@ -33,4 +50,6 @@ class JFXListCell[T]( delegate.updateListView(listView) } + // delegate.cell + } diff --git a/src/main/scala/nova/monadic_sfx/util/controls/JFXListView.scala b/src/main/scala/nova/monadic_sfx/util/controls/JFXListView.scala index 7b8a0b4..790f5a6 100644 --- a/src/main/scala/nova/monadic_sfx/util/controls/JFXListView.scala +++ b/src/main/scala/nova/monadic_sfx/util/controls/JFXListView.scala @@ -25,16 +25,8 @@ class JFXListView[T]( // v.foreach { items() = _ } // } - def items_=( - v: Observable[Seq[T]] - )(implicit s: Scheduler): Unit = { - v - .map { - // case buf: ObservableBuffer[T] => buf - case other => ObservableBuffer.from(other) - } - // .map(myDiff(items(), _)) - .foreach { items() = _ } + def items_=(v: Observable[Seq[T]])(implicit s: Scheduler): Unit = { + v.map(ObservableBuffer.from).foreach(items() = _) } def depth = delegate.depthProperty() diff --git a/src/main/scala/nova/monadic_sfx/util/controls/JFXRippler.scala b/src/main/scala/nova/monadic_sfx/util/controls/JFXRippler.scala new file mode 100644 index 0000000..b1ab7e8 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/controls/JFXRippler.scala @@ -0,0 +1,37 @@ +package nova.monadic_sfx.util.controls +import com.jfoenix.{controls => jfoenixc} +import scalafx.scene.Node +import scalafx.scene.layout.StackPane +import scalafx.scene.paint.Paint + +class JFXRippler( + override val delegate: jfoenixc.JFXRippler = new jfoenixc.JFXRippler +) extends StackPane(delegate) { + import JFXRippler._ + def control = delegate.getControl() + def control_=(v: Node) = delegate.setControl(v) + def enabled_=(v: Boolean) = delegate.setEnabled(v) + def ripplerPos = delegate.getPosition() + def ripplerPos_=(pos: RipplerPos) = delegate.setPosition(pos) + def ripplerDisabled = delegate.ripplerDisabledProperty() + def ripplerDisabled_=(v: Boolean) = delegate.setRipplerDisabled(v) + def ripplerFill = delegate.ripplerFillProperty() + def ripplerFill_=(v: Paint) = delegate.setRipplerFill(v) + def ripplerRecenter = delegate.ripplerRecenterProperty() + def ripplerRecenter_=(v: Boolean) = delegate.setRipplerRecenter(v) + def ripplerRadius = delegate.ripplerRadiusProperty() + def ripplerRadius_=(v: Int) = delegate.setRipplerRadius(v) +} + +object JFXRippler { + abstract class RipplerPos(val delegate: jfoenixc.JFXRippler.RipplerPos) + case object Front extends RipplerPos(jfoenixc.JFXRippler.RipplerPos.FRONT) + case object Back extends RipplerPos(jfoenixc.JFXRippler.RipplerPos.BACK) + object RipplerPos { + implicit def sfxRipplerPos2jfxRipplerPos( + v: RipplerPos + ): jfoenixc.JFXRippler.RipplerPos = v.delegate + } + implicit def sfxRippler2jfxRippler(v: JFXRippler): jfoenixc.JFXRippler = + v.delegate +} diff --git a/src/main/scala/nova/monadic_sfx/util/controls/VideoView.scala b/src/main/scala/nova/monadic_sfx/util/controls/VideoView.scala new file mode 100644 index 0000000..73187a4 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/controls/VideoView.scala @@ -0,0 +1,15 @@ +package nova.monadic_sfx.util.controls + +import scalafx.scene.image.ImageView +import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurfaceFactory.videoSurfaceForImageView +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer + +class VideoView(val mediaPlayer: EmbeddedMediaPlayer) extends ImageView { +// private val _mediaPlayer = ObjectProperty[Option[EmbeddedMediaPlayer]](None) +// def mediaPlayer = _mediaPlayer +// def mediaPlayer_=(v: EmbeddedMediaPlayer): Unit = { +// v.videoSurface().set(videoSurfaceForImageView(this)) +// _mediaPlayer.value = Some(v) +// } + mediaPlayer.videoSurface().set(videoSurfaceForImageView(this)) +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/store/Middlewares.scala b/src/main/scala/nova/monadic_sfx/util/reactive/store/Middlewares.scala index 7c1939e..5b961da 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/store/Middlewares.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/store/Middlewares.scala @@ -13,9 +13,6 @@ import io.odin.formatter.options.ThrowableFormat import io.odin.meta.Render import monix.bio.Task import monix.reactive.Observable -// object Middleware { -// def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob -// } @JsonCodec final case class StoreInfo[A]( @@ -34,19 +31,12 @@ object StoreInfo { object Middlewares { - // val encoder: Encoder[LoggerMessage] = - // Encoder.forProduct1("message")(m => m.message.value) - val format = create(ThrowableFormat.Default, PositionFormat.Full) def create( throwableFormat: ThrowableFormat, positionFormat: PositionFormat - ): io.odin.formatter.Formatter = { - // val encoder: Encoder[LoggerMessage] = - // Encoder.forProduct1("message")(m => m.message.value) - (msg: LoggerMessage) => msg.message.value - } + ): io.odin.formatter.Formatter = (msg: LoggerMessage) => msg.message.value def actionStateLoggerMiddleware[A, M]( logger: Logger[Task] diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/store/Store.scala b/src/main/scala/nova/monadic_sfx/util/reactive/store/Store.scala index dbcc39d..f27cc06 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/store/Store.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/store/Store.scala @@ -1,12 +1,8 @@ package nova.monadic_sfx.util.reactive.store -import java.time.LocalDateTime - -import io.circe.Encoder -import io.odin.Logger import monix.bio.Task import monix.eval.Coeval -import monix.reactive.Observable +import monix.reactive.Observer import monix.reactive.OverflowStrategy import monix.reactive.subjects.ConcurrentSubject @@ -34,73 +30,26 @@ object Store { } } - val obs = Observable.suspend( - subject - .scanEval0F[Coeval, (A, M)]( - Coeval.pure(initialAction -> initialState) - )(fold) - .behavior(initialAction -> initialState) - .refCount - ) - - val res = middlewares.foldLeft(obs) { - case (obs, middleware) => middleware(obs) - } - - MonixProSubject.from( - subject, - res - ) - } - } - - def createJsonL[A: Encoder, M]( - initialAction: A, - initialState: M, - reducer: Reducer[A, M], - storeName: String, - logger: Logger[Task], - middlewares: Seq[Middleware[A, M]] = Seq.empty, - overflowStrategy: OverflowStrategy.Synchronous[A] = - OverflowStrategy.DropOld(50) - ): Task[Store[A, M]] = - Task.deferAction { implicit s => - Task { - val subject = ConcurrentSubject.publish[A](overflowStrategy) - - val fold: ((A, M), A) => Task[(A, M)] = { - case ((_, state), action) => - Task { - val (newState, effects) = reducer(state, action) - - effects.subscribe(subject.onNext _) - - action -> newState - } - } - val obs = subject - .doOnNextF(action => - Task(LocalDateTime.now()).flatMap(curTime => - logger.debug( - StoreInfo(storeName, action, curTime) - ) - ) - ) - // .doOnNextF(action => Coeval(println(action))) - .scanEvalF[Task, (A, M)](Task.pure(initialAction -> initialState))( - fold - ) + .scanEval0F[Coeval, (A, M)]( + Coeval.pure(initialAction -> initialState) + )(fold) + + val res = middlewares + .foldLeft(obs) { + case (obs, middleware) => middleware(obs) + } + .doOnNextF(i => Coeval(println(s"Emitted item 1: $i"))) .behavior(initialAction -> initialState) .refCount - // val res = middlewares.foldLeft(obs) { - // case (obs, middleware) => middleware(obs) - // } + res.subscribe(Observer.empty) + + // .doOnNextF(i => Coeval(println(s"Emitted item 2: $i"))) MonixProSubject.from( subject, - obs + res ) } } diff --git a/src/main/scala/nova/monadic_sfx/util/test.txt b/src/main/scala/nova/monadic_sfx/util/test.txt new file mode 100644 index 0000000..35dbb09 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/test.txt @@ -0,0 +1,32 @@ +Vector() +0 +//push 1 +Vector(1) +0 +//push 2 +Vector(1,2) +1 +//forward +Vector(1,2) +1 +//backward +Vector(1,2) +0 +//backward +Vector(1,2) +0 +//push 3 +Vector(1,2,3) +sp 2 +//push 4 +Vector(1,2,3,4) +sp 3 +//push 5 +Vector(1,2,3,4,5) +sp 4 +//backward +Vector(1,2,3,4,5) +sp 3 +//push 6 +Vector(1,2,3,4,6) +sp 3 diff --git a/src/test/scala/HistoryTest.scala b/src/test/scala/HistoryTest.scala new file mode 100644 index 0000000..30e0ee7 --- /dev/null +++ b/src/test/scala/HistoryTest.scala @@ -0,0 +1,107 @@ +import org.scalatest.funsuite.AnyFunSuite +import nova.monadic_sfx.util.History +import monix.execution.atomic.Atomic +class HistoryTest extends AnyFunSuite { + val historyRef = Atomic(History(0)) + + test("init") { + val h = historyRef.get() + assert(h.state.values == Vector(0)) + assert(h.state.sp == 0) + assert(h.current == 0) + } + + test("push 1") { + val h = historyRef.transformAndGet(_.push(1)) + // logger.debug(mutHistory.ints.toString) + assert(h.state.values == Vector(0, 1)) + assert(h.state.values.length - 1 == h.state.sp) + assert(h.current == 1) + } + + test("push 2") { + val h = historyRef.transformAndGet(_.push(2)) + // logger.debug(mutHistory.ints.toString) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.values.length - 1 == h.state.sp) + assert(h.current == 2) + } + + test("first forward") { + // logger.debug(mutHistory.ints.toString) + // logger.debug(mutHistory.sp.toString) + val h = historyRef.transformAndGet(_.forward) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.sp == 2) + assert(h.current == 2) + } + + test("second forward") { + // logger.debug(mutHistory.ints.toString) + // logger.debug(mutHistory.sp.toString) + val h = historyRef.transformAndGet(_.forward) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.sp == 2) + assert(h.current == 2) + } + test("first backward") { + val h = historyRef.transformAndGet(_.backward) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.sp == 1) + assert(h.current == 1) + } + test("second backward") { + val h = historyRef.transformAndGet(_.backward) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.sp == 0) + assert(h.current == 0) + } + test("third backward") { + val h = historyRef.transformAndGet(_.backward) + assert(h.state.values == Vector(0, 1, 2)) + assert(h.state.sp == 0) + assert(h.current == 0) + } + test("push 3") { + val h = historyRef.transformAndGet(_.push(3)) + // logger.debug(mutHistory.ints.toString) + assert(h.state.values == Vector(3)) + assert(h.state.sp == 0) + assert(h.current == 3) + } + test("fourth backward") { + val h = historyRef.transformAndGet(_.backward) + assert(h.state.values == Vector(3)) + assert(h.state.sp == 0) + assert(h.current == 3) + } + test("lastly") { + val h1 = historyRef.transformAndGet( + _.push(4) + .push(5) + .push(6) + .push(7) + .push(8) + ) + assert(h1.state.values == Vector(3, 4, 5, 6, 7, 8)) + assert(h1.state.sp == 5) + assert(h1.current == 8) + val h2 = historyRef.transformAndGet(_.backward.backward) + assert(h2.current == 6) + assert(h2.state.sp == 3) + val h3 = historyRef.transformAndGet(_.push(9)) + assert(h3.state.values == Vector(3, 4, 5, 9)) + assert(h3.state.sp == 3) + assert(h3.current == 9) + for (i <- 1 to 4) historyRef.transform(_.backward) + // assert(h.current == None) + // assert(h.ints == Vector.empty) + val h4 = historyRef.get() + assert(h4.state.sp == 0) + assert(h4.current == 3) + val h5 = historyRef.transformAndGet(_.push(1)) + assert(h5.current == 1) + assert(h5.state.sp == 0) + + } +} diff --git a/src/test/scala/ProSubjectTest.scala b/src/test/scala/ProSubjectTest.scala new file mode 100644 index 0000000..52f2fb7 --- /dev/null +++ b/src/test/scala/ProSubjectTest.scala @@ -0,0 +1,83 @@ +import org.scalatest.funsuite.AnyFunSuite +import monix.eval.Task +import monix.reactive.subjects.ConcurrentSubject +import monix.eval.Coeval +import com.typesafe.scalalogging.LazyLogging +import monix.execution.Scheduler.global +import nova.monadic_sfx.util.reactive.store.MonixProSubject +import scala.concurrent.duration._ +import scala.concurrent.Await +import monix.reactive.Observable + +class ProSubjectTest extends AnyFunSuite with LazyLogging { + test("task1") { + implicit val s = global + val x = Await.result(task1.runToFuture, 10.seconds) + assert(x == 1) + } + + test("task2") { + implicit val s = global + val x = Await.result(task2.runToFuture, 10.seconds) + } + + def task1 = + Task + .deferAction(implicit s => + Task { + val sub = ConcurrentSubject.publish[Int] + // val obs = sub.scan0(0)(_ + _).behavior(2).refCount + val obs = + sub + .scanEval0F(Task.pure(0))((a, b) => Task(a + b)) + .behavior(2) + .refCount + .doOnNextF(i => Coeval(println(s"Emitted1: $i"))) + // type MonixProSubject[-I, +O] = Observable[O] with Observer[I] + // def from[I, O]( + // observer: Observer[I], + // observable: Observable[O] + // ): MonixProSubject[I, O] = + // new Observable[O] with Observer[I] { + // override def onNext(elem: I): Future[Ack] = observer.onNext(elem) + // override def onError(ex: Throwable): Unit = observer.onError(ex) + // override def onComplete(): Unit = observer.onComplete() + // override def unsafeSubscribeFn( + // subscriber: Subscriber[O] + // ): Cancelable = + // observable.unsafeSubscribeFn(subscriber) + // } + MonixProSubject.from(sub, obs) + }.flatMap { + case ps => + Task { + ps + .doOnNextF(i => Coeval(println(s"Emitted item 1: $i"))) + .subscribe() + + } >> Task { + ps + .doOnNextF(i => Coeval(println(s"Emitted item 2: $i"))) + .subscribe() + } >> Task { + (0 to 5).foreach { i => ps.onNext(i) } + } >> Task(1) + } + ) + + val task2 = Task.deferAction(implicit s => + Task( + Observable(1, 2, 3, 4, 5) + .scan0(0)(_ + _) + .behavior(2) + .refCount + // .doOnNextF(i => Coeval(println(s"Emitted2: $i"))) + .delayExecution(10.millis) + ) + .flatMap(res => + Task(res.doOnNextF(i => Coeval(println(s"1: $i"))).subscribe()).start >> + Task(res.doOnNextF(i => Coeval(println(s"2: $i"))).subscribe()).start + ) + ) + +} diff --git a/src/test/scala/WebSocketTest.scala b/src/test/scala/WebSocketTest.scala new file mode 100644 index 0000000..cb41ab0 --- /dev/null +++ b/src/test/scala/WebSocketTest.scala @@ -0,0 +1,61 @@ +import org.scalatest.funsuite.AnyFunSuite +import sttp.client._ +import sttp.client.httpclient.monix.HttpClientMonixBackend +import monix.execution.Scheduler +import sttp.client.httpclient.monix.MonixWebSocketHandler +import org.scalatest.BeforeAndAfterAll +import monix.eval.Task +import sttp.model.ws.WebSocketFrame +import scala.concurrent.duration._ +import nova.monadic_sfx.implicits._ +import monix.catnap.ConcurrentQueue +class WebSocketTest extends AnyFunSuite with BeforeAndAfterAll { + implicit val sched = Scheduler.global + implicit val backend = HttpClientMonixBackend().runSyncUnsafe() + val ws = basicRequest + .get(uri"ws://localhost:6789") + .openWebsocketF(MonixWebSocketHandler()) + .map(_.result) + .runSyncUnsafe() + + test("open websocket") { + (for { + isOpen <- ws.isOpen + _ <- Task(assert(isOpen == true)) + _ <- Task(assert(isOpen != false)) + } yield ()).runSyncUnsafe(10.seconds) + } + + test("send message") { + (for { + _ <- ws.send(WebSocketFrame.text("Test Message")) + // _ <- Task.sleep(1.second) + // _ <- Task(assert(isOpen == true)) + } yield ()).runSyncUnsafe(10.seconds) + } + + test("receive messages observable") { + (for { + queue <- ConcurrentQueue.bounded[Task, Int](10) + _ <- + ws.observableSource + .filter(_.isRight) + .map(_.right.get) + .doOnNext(s => + Task(println(s"Received item: $s")) >> + s.toIntOption.fold(Task.unit)(queue.offer) + ) + .take(5) + .completedL + .timeout(5.seconds) + items <- queue.drain(5, 5).timeout(5.seconds) + _ <- Task(assert(items == Seq(1, 2, 3, 4, 5))) + _ <- Task(assert(items != Seq(1, 2, 3, 4, 5, 6))) + } yield ()).runSyncUnsafe(10.seconds) + } + + override def afterAll() = { + ws.close.runSyncUnsafe() + backend.close() + } +} diff --git a/src/test/scala/WebSocketTestServer.py b/src/test/scala/WebSocketTestServer.py new file mode 100644 index 0000000..5292773 --- /dev/null +++ b/src/test/scala/WebSocketTestServer.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +import asyncio +import websockets + + +async def server(websocket, path): + while True: + # Get received data from websocket + data = await websocket.recv() + + # Send response back to client to acknowledge receiving message + print("Received data {}".format(data)) + # await websocket.send(data) + # for i in range(1, 6): + # print("Sending item: {}".format(i)) + # await websocket.send(str(i)) + +# Create websocket server +start_server = websockets.serve(server, "localhost", 6789) + +# Start and run websocket server forever +asyncio.get_event_loop().run_until_complete(start_server) +print("Starting websocket server") +asyncio.get_event_loop().run_forever()