gse
This commit is contained in:
parent
2df921807d
commit
81765c6dcd
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ metals.sbt
|
||||
/project/project
|
||||
|
||||
.bsp
|
||||
.attach_pid**
|
||||
|
@ -8,7 +8,7 @@ version := "14-R19"
|
||||
scalaVersion := "2.13.4"
|
||||
|
||||
// Add dependency on ScalaFX library
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "14-R19"
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "15.0.1-R20"
|
||||
resolvers += Resolver.sonatypeRepo("snapshots")
|
||||
|
||||
enablePlugins(JavaFxPlugin)
|
||||
@ -26,7 +26,8 @@ libraryDependencies ++= Seq(
|
||||
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
|
||||
"com.softwaremill.quicklens" %% "quicklens" % "1.6.1",
|
||||
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
|
||||
"com.softwaremill.macwire" %% "util" % "2.3.7",
|
||||
// "com.softwaremill.macwire" %% "util" % "2.3.7",
|
||||
"com.softwaremill.common" %% "tagging" % "2.2.1",
|
||||
"com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided",
|
||||
"com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided",
|
||||
"com.github.valskalla" %% "odin-monix" % "0.9.1",
|
||||
@ -48,7 +49,9 @@ libraryDependencies ++= Seq(
|
||||
"fr.brouillard.oss" % "cssfx" % "11.4.0",
|
||||
"com.lihaoyi" %% "sourcecode" % "0.2.1",
|
||||
"eu.timepit" %% "refined" % "0.9.19",
|
||||
"org.scalatest" %% "scalatest" % "3.2.2" % "test"
|
||||
"org.scalatest" %% "scalatest" % "3.2.2" % "test",
|
||||
"uk.co.caprica" % "vlcj" % "4.7.0",
|
||||
"uk.co.caprica" % "vlcj-javafx" % "1.0.2"
|
||||
)
|
||||
|
||||
scalacOptions ++= Seq(
|
||||
|
@ -1,2 +1,2 @@
|
||||
sbt.version=1.4.3
|
||||
sbt.version=1.4.5
|
||||
|
||||
|
@ -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;
|
||||
.text-white {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.jfx-hamburger StackPane {
|
||||
-fx-pref-width: 40px;
|
||||
-fx-pref-height: 7px;
|
||||
-fx-background-color: #D63333;
|
||||
-fx-background-radius: 5px;
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Input Demo */
|
||||
|
||||
.text-field {
|
||||
-fx-max-width: 300;
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
.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;
|
||||
}
|
||||
.clear-list-view .scroll-bar .increment-arrow,
|
||||
.clear-list-view .scroll-bar .decrement-arrow {
|
||||
-fx-shape: " ";
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.jfx-decorator {
|
||||
-fx-decorator-color: RED;
|
||||
.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;
|
||||
}
|
||||
|
||||
.jfx-decorator .jfx-decorator-buttons-container {
|
||||
-fx-background-color: -fx-decorator-color;
|
||||
.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;
|
||||
}
|
||||
|
||||
.jfx-decorator .resize-border {
|
||||
-fx-border-color: -fx-decorator-color;
|
||||
-fx-border-width: 0 4 4 4;
|
||||
.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;
|
||||
}
|
||||
|
||||
.jfx-text-area, .text-area {
|
||||
-fx-font-weight: BOLD;
|
||||
.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;
|
||||
}
|
||||
.scroll-pane .scroll-bar .increment-arrow,
|
||||
.scroll-pane .scroll-bar .decrement-arrow {
|
||||
-fx-shape: " ";
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error {
|
||||
-jfx-focus-color: #D34336;
|
||||
-jfx-unfocus-color: #D34336;
|
||||
.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;
|
||||
}
|
||||
|
||||
.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
|
||||
-fx-text-fill: #D34336;
|
||||
-fx-font-size: 0.75em;
|
||||
.scroll-pane {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
|
||||
-fx-text-fill: #D34336;
|
||||
-fx-font-size: 1em;
|
||||
.scroll-pane > .viewport {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
/* Progress Bar Demo */
|
||||
|
||||
.progress-bar > .bar {
|
||||
-fx-min-width: 500;
|
||||
.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-progress-bar > .bar {
|
||||
-fx-min-width: 500;
|
||||
.clear-list-view .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.jfx-progress-bar {
|
||||
-fx-progress-color: #0F9D58;
|
||||
-fx-stroke-width: 3;
|
||||
.clear-list-view .list-cell:selected {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.2);
|
||||
-fx-text-fill: white;
|
||||
-fx-background-radius: 20px;
|
||||
}
|
||||
|
||||
/* Icons Demo */
|
||||
.icon {
|
||||
-fx-text-fill: #FE774D;
|
||||
-fx-padding: 10;
|
||||
-fx-cursor: hand;
|
||||
.clear-list-view .list-cell:hover {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.1);
|
||||
-fx-text-fill: white;
|
||||
-fx-background-radius: 20px;
|
||||
}
|
||||
|
||||
.icons-rippler {
|
||||
-jfx-rippler-fill: BLUE;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
.clear-list-view .list-cell:selected:hover {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.3);
|
||||
-fx-text-fill: white;
|
||||
-fx-background-radius: 20px;
|
||||
}
|
||||
|
||||
.icons-rippler:hover {
|
||||
-fx-cursor: hand;
|
||||
.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-check-box {
|
||||
-fx-font-weight: BOLD;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box {
|
||||
-jfx-checked-color: RED;
|
||||
-jfx-unchecked-color: BLACK;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
.button {
|
||||
-fx-padding: 0.7em 0.57em;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
||||
}
|
||||
|
||||
.mylistview .scroll-bar .increment-arrow,
|
||||
.mylistview .scroll-bar .decrement-arrow {
|
||||
-fx-shape: " ";
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.jfx-list-cell-container {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.jfx-list-cell-container > .label {
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.jfx-list-cell {
|
||||
-fx-background-insets: 0.0;
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.jfx-list-cell:odd, .jfx-list-cell:even {
|
||||
-fx-background-color: WHITE;
|
||||
}
|
||||
|
||||
.jfx-list-cell:filled:hover {
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
.jfx-list-cell .jfx-rippler {
|
||||
-jfx-rippler-fill: BLUE;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
@ -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 ()
|
||||
|
||||
|
@ -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"
|
||||
})
|
||||
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)
|
||||
})
|
||||
)
|
||||
// val players = mediaPlayerFactory.mediaPlayers
|
||||
// val videoPlayerController = players.newEmbeddedMediaPlayer()
|
||||
// mediaPlayer.controller.videoSurface.set(
|
||||
// videoSurfaceForImageView(videoView)
|
||||
// )
|
||||
|
||||
mainSceneNode <- Task.deferAction(implicit s =>
|
||||
Task(new StackPane { root =>
|
||||
// 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
|
||||
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))
|
||||
}
|
||||
},
|
||||
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"))
|
||||
padding = Insets(20)
|
||||
})
|
||||
d.styleClass ++= Seq("text-white")
|
||||
onAction = () => d.show(root)
|
||||
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
|
||||
}
|
||||
})
|
||||
)
|
||||
} yield mainSceneNode
|
||||
}
|
||||
|
||||
class TestModel(_name: String, _age: Int) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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]) =
|
||||
|
@ -1,8 +1,17 @@
|
||||
package nova.monadic_sfx
|
||||
|
||||
import monix.eval.TaskLike
|
||||
import monix.reactive.Observable
|
||||
|
||||
package object implicits
|
||||
extends MySfxObservableImplicits
|
||||
with JavaFXMonixObservables
|
||||
with JavaFXMonixObservables {
|
||||
implicit class SttpWsOps[F[_]](private val ws: sttp.client.ws.WebSocket[F])
|
||||
extends AnyVal {
|
||||
def observableSource(implicit F: TaskLike[F]) =
|
||||
Observable.repeatEvalF(ws.receiveText())
|
||||
}
|
||||
}
|
||||
|
||||
// implicit class NodeExt(val node: Node) {
|
||||
// def lookup2[T <: SFXDelegate[_]](
|
||||
|
71
src/main/scala/nova/monadic_sfx/ui/FXComponent.scala
Normal file
71
src/main/scala/nova/monadic_sfx/ui/FXComponent.scala
Normal file
@ -0,0 +1,71 @@
|
||||
package nova.monadic_sfx.ui
|
||||
|
||||
import javafx.{scene => jfxc}
|
||||
import monix.bio.Task
|
||||
import monix.execution.Cancelable
|
||||
import monix.execution.cancelables.CompositeCancelable
|
||||
import monix.reactive.subjects.ConcurrentSubject
|
||||
import nova.monadic_sfx.implicits._
|
||||
import scalafx.beans.property.ObjectProperty
|
||||
import scalafx.scene.Node
|
||||
import scalafx.scene.Parent
|
||||
import scalafx.scene.control.TextField
|
||||
import scalafx.scene.text.Font
|
||||
import cats.effect.Resource
|
||||
|
||||
final class FXComponent private (
|
||||
val rootNode: Parent,
|
||||
val cancelable: Cancelable
|
||||
)
|
||||
|
||||
object FXComponent {
|
||||
def acquire(f: CompositeCancelable => Task[Parent]) =
|
||||
for {
|
||||
c <- Task(CompositeCancelable())
|
||||
p <- f(c)
|
||||
} yield new FXComponent(p, c)
|
||||
|
||||
def fxComponent2Node(
|
||||
component: FXComponent
|
||||
)(implicit c: CompositeCancelable): Node = {
|
||||
c += component.cancelable
|
||||
component.rootNode
|
||||
}
|
||||
|
||||
def resource(f: CompositeCancelable => Task[Parent]) =
|
||||
Resource.make(acquire(f))(comp => Task(comp.cancelable.cancel()))
|
||||
}
|
||||
|
||||
object TestFXComp {
|
||||
val testComp =
|
||||
FXComponent.resource { implicit c =>
|
||||
Task.deferAction { implicit s =>
|
||||
Task {
|
||||
val sub = ConcurrentSubject.publish[jfxc.text.Font]
|
||||
val f = ObjectProperty(Font("hmm"))
|
||||
sub.onNext(f())
|
||||
new TextField {
|
||||
font <-- sub
|
||||
font <== f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// val x = for {
|
||||
// comp <- testComp
|
||||
// res <- FXComponent.make { implicit c =>
|
||||
// Task.deferAction { implicit s =>
|
||||
// Task {
|
||||
// new BorderPane {
|
||||
// val sub = ConcurrentSubject.publish[jfxc.text.Font]
|
||||
// center = FXComponent.fxComponent2Node(comp)
|
||||
// bottom = new TextField {
|
||||
// font <-- sub
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } yield res
|
||||
}
|
@ -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 {
|
||||
_ <- logger.info("Starting FX App")
|
||||
fib <-
|
||||
Task(internal.main(Array.empty)).start.executeOn(schedulers.blocking)
|
||||
_ <- Task.sleep(200.millis)
|
||||
_ <- Task(internal.stage = stage)
|
||||
.executeOn(schedulers.fx)
|
||||
.delayExecution(delay)
|
||||
} yield (this, fib)
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
37
src/main/scala/nova/monadic_sfx/util/BoundedStack.scala
Normal file
37
src/main/scala/nova/monadic_sfx/util/BoundedStack.scala
Normal file
@ -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)
|
||||
|
||||
}
|
18
src/main/scala/nova/monadic_sfx/util/CssPath.scala
Normal file
18
src/main/scala/nova/monadic_sfx/util/CssPath.scala
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
}
|
47
src/main/scala/nova/monadic_sfx/util/WebSocket.scala
Normal file
47
src/main/scala/nova/monadic_sfx/util/WebSocket.scala
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package nova.monadic_sfx.util.controls
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
@ -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]
|
||||
|
@ -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,18 +30,22 @@ object Store {
|
||||
}
|
||||
}
|
||||
|
||||
val obs = Observable.suspend(
|
||||
subject
|
||||
.scanEval0F[Coeval, (A, M)](
|
||||
Coeval.pure(initialAction -> initialState)
|
||||
)(fold)
|
||||
.behavior(initialAction -> initialState)
|
||||
.refCount
|
||||
)
|
||||
val obs = subject
|
||||
.scanEval0F[Coeval, (A, M)](
|
||||
Coeval.pure(initialAction -> initialState)
|
||||
)(fold)
|
||||
|
||||
val res = middlewares.foldLeft(obs) {
|
||||
case (obs, middleware) => middleware(obs)
|
||||
}
|
||||
val res = middlewares
|
||||
.foldLeft(obs) {
|
||||
case (obs, middleware) => middleware(obs)
|
||||
}
|
||||
.doOnNextF(i => Coeval(println(s"Emitted item 1: $i")))
|
||||
.behavior(initialAction -> initialState)
|
||||
.refCount
|
||||
|
||||
res.subscribe(Observer.empty)
|
||||
|
||||
// .doOnNextF(i => Coeval(println(s"Emitted item 2: $i")))
|
||||
|
||||
MonixProSubject.from(
|
||||
subject,
|
||||
@ -54,55 +54,4 @@ object Store {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
.behavior(initialAction -> initialState)
|
||||
.refCount
|
||||
|
||||
// val res = middlewares.foldLeft(obs) {
|
||||
// case (obs, middleware) => middleware(obs)
|
||||
// }
|
||||
|
||||
MonixProSubject.from(
|
||||
subject,
|
||||
obs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
32
src/main/scala/nova/monadic_sfx/util/test.txt
Normal file
32
src/main/scala/nova/monadic_sfx/util/test.txt
Normal file
@ -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
|
107
src/test/scala/HistoryTest.scala
Normal file
107
src/test/scala/HistoryTest.scala
Normal file
@ -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)
|
||||
|
||||
}
|
||||
}
|
83
src/test/scala/ProSubjectTest.scala
Normal file
83
src/test/scala/ProSubjectTest.scala
Normal file
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
}
|
61
src/test/scala/WebSocketTest.scala
Normal file
61
src/test/scala/WebSocketTest.scala
Normal file
@ -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()
|
||||
}
|
||||
}
|
25
src/test/scala/WebSocketTestServer.py
Normal file
25
src/test/scala/WebSocketTestServer.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user