gse
This commit is contained in:
parent
2df921807d
commit
81765c6dcd
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ metals.sbt
|
|||||||
/project/project
|
/project/project
|
||||||
|
|
||||||
.bsp
|
.bsp
|
||||||
|
.attach_pid**
|
||||||
|
@ -8,7 +8,7 @@ version := "14-R19"
|
|||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.4"
|
||||||
|
|
||||||
// Add dependency on ScalaFX library
|
// Add dependency on ScalaFX library
|
||||||
libraryDependencies += "org.scalafx" %% "scalafx" % "14-R19"
|
libraryDependencies += "org.scalafx" %% "scalafx" % "15.0.1-R20"
|
||||||
resolvers += Resolver.sonatypeRepo("snapshots")
|
resolvers += Resolver.sonatypeRepo("snapshots")
|
||||||
|
|
||||||
enablePlugins(JavaFxPlugin)
|
enablePlugins(JavaFxPlugin)
|
||||||
@ -26,7 +26,8 @@ libraryDependencies ++= Seq(
|
|||||||
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
|
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
|
||||||
"com.softwaremill.quicklens" %% "quicklens" % "1.6.1",
|
"com.softwaremill.quicklens" %% "quicklens" % "1.6.1",
|
||||||
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
|
"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" %% "macros" % "2.3.6" % "provided",
|
||||||
"com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided",
|
"com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided",
|
||||||
"com.github.valskalla" %% "odin-monix" % "0.9.1",
|
"com.github.valskalla" %% "odin-monix" % "0.9.1",
|
||||||
@ -48,7 +49,9 @@ libraryDependencies ++= Seq(
|
|||||||
"fr.brouillard.oss" % "cssfx" % "11.4.0",
|
"fr.brouillard.oss" % "cssfx" % "11.4.0",
|
||||||
"com.lihaoyi" %% "sourcecode" % "0.2.1",
|
"com.lihaoyi" %% "sourcecode" % "0.2.1",
|
||||||
"eu.timepit" %% "refined" % "0.9.19",
|
"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(
|
scalacOptions ++= Seq(
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
sbt.version=1.4.3
|
sbt.version=1.4.5
|
||||||
|
|
||||||
|
@ -1,342 +1,123 @@
|
|||||||
.root {
|
.root {
|
||||||
-fx-font-family: Roboto;
|
-fx-font-family: Roboto;
|
||||||
src: "/resources/roboto/Roboto-Regular.ttf";
|
src: "/resources/roboto/Roboto-Regular.ttf";
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-color: #14161d;
|
||||||
|
/* rgb(38,38,38) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Burgers Demo */
|
.text-white {
|
||||||
|
-fx-text-fill: white;
|
||||||
.jfx-hamburger {
|
|
||||||
-fx-spacing: 5;
|
|
||||||
-fx-cursor: hand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-hamburger StackPane {
|
.clear-list-view .scroll-bar:horizontal .track,
|
||||||
-fx-pref-width: 40px;
|
.clear-list-view .scroll-bar:vertical .track {
|
||||||
-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icons Demo */
|
|
||||||
.icon {
|
|
||||||
-fx-text-fill: #FE774D;
|
|
||||||
-fx-padding: 10;
|
|
||||||
-fx-cursor: hand;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons-rippler {
|
|
||||||
-jfx-rippler-fill: BLUE;
|
|
||||||
-jfx-mask-type: CIRCLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons-rippler:hover {
|
|
||||||
-fx-cursor: hand;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-background-color: transparent;
|
||||||
-fx-border-color: transparent;
|
-fx-border-color: transparent;
|
||||||
-fx-background-radius: 0em;
|
-fx-background-radius: 0em;
|
||||||
-fx-border-radius: 2em;
|
-fx-border-radius: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The increment and decrement button CSS class of scrollbar */
|
.clear-list-view .scroll-bar:horizontal .increment-button,
|
||||||
.mylistview .scroll-bar:horizontal .increment-button,
|
.clear-list-view .scroll-bar:horizontal .decrement-button {
|
||||||
.mylistview .scroll-bar:horizontal .decrement-button {
|
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-background-radius: 0em;
|
-fx-background-radius: 0em;
|
||||||
-fx-padding: 0 0 10 0;
|
-fx-padding: 0 0 10 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The increment and decrement button CSS class of scrollbar */
|
.clear-list-view .scroll-bar:vertical .increment-button,
|
||||||
|
.clear-list-view .scroll-bar:vertical .decrement-button {
|
||||||
.mylistview .scroll-bar:vertical .increment-button,
|
|
||||||
.mylistview .scroll-bar:vertical .decrement-button {
|
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-background-radius: 0em;
|
-fx-background-radius: 0em;
|
||||||
-fx-padding: 0 10 0 0;
|
-fx-padding: 0 10 0 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.clear-list-view .scroll-bar .increment-arrow,
|
||||||
.mylistview .scroll-bar .increment-arrow,
|
.clear-list-view .scroll-bar .decrement-arrow {
|
||||||
.mylistview .scroll-bar .decrement-arrow {
|
|
||||||
-fx-shape: " ";
|
-fx-shape: " ";
|
||||||
-fx-padding: 0;
|
-fx-padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
|
.clear-list-view .scroll-bar:horizontal .thumb,
|
||||||
.mylistview .scroll-bar:horizontal .thumb,
|
.clear-list-view .scroll-bar:vertical .thumb {
|
||||||
.mylistview .scroll-bar:vertical .thumb {
|
|
||||||
-fx-background-color: derive(black, 90%);
|
-fx-background-color: derive(black, 90%);
|
||||||
-fx-background-insets: 2, 0, 0;
|
-fx-background-insets: 2, 0, 0;
|
||||||
-fx-background-radius: 2em;
|
-fx-background-radius: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-list-cell-container {
|
.scroll-pane .scroll-bar:horizontal .track,
|
||||||
-fx-alignment: center-left;
|
.scroll-pane .scroll-bar:vertical .track {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
-fx-background-radius: 0em;
|
||||||
|
-fx-border-radius: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-list-cell-container > .label {
|
.scroll-pane .scroll-bar:horizontal .increment-button,
|
||||||
-fx-text-fill: BLACK;
|
.scroll-pane .scroll-bar:horizontal .decrement-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-radius: 0em;
|
||||||
|
-fx-padding: 0 0 10 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-list-cell:odd:selected > .jfx-rippler > StackPane, .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
|
.scroll-pane .scroll-bar:vertical .increment-button,
|
||||||
-fx-background-color: rgba(0, 0, 255, 0.2);
|
.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-list-cell {
|
.scroll-pane .scroll-bar:horizontal .thumb,
|
||||||
-fx-background-insets: 0.0;
|
.scroll-pane .scroll-bar:vertical .thumb {
|
||||||
-fx-text-fill: BLACK;
|
-fx-background-color: derive(black, 90%);
|
||||||
|
-fx-background-insets: 2, 0, 0;
|
||||||
|
-fx-background-radius: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-list-cell:odd, .jfx-list-cell:even {
|
.scroll-pane {
|
||||||
-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;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animated-option-button {
|
.scroll-pane > .viewport {
|
||||||
-fx-background-color: #F1F1F1;
|
-fx-background-color: transparent;
|
||||||
-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 {
|
.clear-list-view {
|
||||||
-fx-pref-width: 60px;
|
-fx-background-color: rgba(28, 28, 28, 28, 0.4);
|
||||||
-fx-background-color: #0F9D58;
|
-fx-text-fill: white;
|
||||||
-fx-background-radius: 60px;
|
-fx-background-radius: 20px;
|
||||||
-fx-pref-height: 60px;
|
-fx-padding: 5px 10px;
|
||||||
-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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clear-list-view .list-cell {
|
||||||
.animated-option-sub-button {
|
-fx-background-color: transparent;
|
||||||
-fx-background-color: #43609C;
|
-fx-text-fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animated-option-sub-button2 {
|
.clear-list-view .list-cell:selected {
|
||||||
-fx-background-color: rgb(203, 104, 96);
|
-fx-background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-view .menu-item:focused {
|
.clear-list-view .list-cell:hover {
|
||||||
-fx-background-color: -fx-tree-table-color;
|
-fx-background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-view .menu-item .label {
|
.clear-list-view .list-cell:selected:hover {
|
||||||
-fx-padding: 5 0 5 0;
|
-fx-background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clear-list-view:focused .list-cell:selected {
|
||||||
|
-fx-background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 20px;
|
||||||
|
}
|
||||||
|
@ -9,10 +9,11 @@ import cats.effect.Resource
|
|||||||
import com.softwaremill.macwire._
|
import com.softwaremill.macwire._
|
||||||
import io.odin._
|
import io.odin._
|
||||||
import nova.monadic_sfx.executors._
|
import nova.monadic_sfx.executors._
|
||||||
|
import nova.monadic_sfx.util.MediaPlayerResource
|
||||||
// import nova.monadic_sfx.util.IOUtils._
|
// import nova.monadic_sfx.util.IOUtils._
|
||||||
// import sttp.client.httpclient.monix.HttpClientMonixBackend
|
// import sttp.client.httpclient.monix.HttpClientMonixBackend
|
||||||
object Main extends MainModule with BIOApp {
|
object Main extends MainModule with BIOApp {
|
||||||
lazy val schedulers = new Schedulers()
|
val schedulers = new Schedulers()
|
||||||
|
|
||||||
override def scheduler: Scheduler = schedulers.async
|
override def scheduler: Scheduler = schedulers.async
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ object Main extends MainModule with BIOApp {
|
|||||||
// toIO(HttpClientMonixBackend()(schedulers.async))
|
// toIO(HttpClientMonixBackend()(schedulers.async))
|
||||||
// )(c => toIO(c.close()))
|
// )(c => toIO(c.close()))
|
||||||
// actorSystem <- actorSystemResource(logger)
|
// actorSystem <- actorSystemResource(logger)
|
||||||
|
MediaPlayerResource <- MediaPlayerResource()
|
||||||
_ <- Resource.liftF(wire[MainApp].program)
|
_ <- Resource.liftF(wire[MainApp].program)
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
|
@ -4,12 +4,15 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
import scala.util.Random
|
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 com.softwaremill.macwire._
|
||||||
import io.odin.Logger
|
import io.odin.Logger
|
||||||
import monix.bio.IO
|
import monix.bio.IO
|
||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
import monix.eval.Coeval
|
import monix.eval.Coeval
|
||||||
|
import monix.execution.cancelables.CompositeCancelable
|
||||||
import monix.{eval => me}
|
import monix.{eval => me}
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
import nova.monadic_sfx.executors.Schedulers
|
||||||
import nova.monadic_sfx.implicits._
|
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.router.Page
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListStore
|
import nova.monadic_sfx.ui.components.todo.TodoListStore
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListView
|
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.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.gerweck.scalafx.util._
|
||||||
import org.kordamp.bootstrapfx.BootstrapFX
|
import org.kordamp.bootstrapfx.BootstrapFX
|
||||||
import scalafx.Includes._
|
import scalafx.Includes._
|
||||||
import scalafx.application.JFXApp.PrimaryStage
|
import scalafx.application.JFXApp3.PrimaryStage
|
||||||
import scalafx.beans.property.ObjectProperty
|
import scalafx.beans.property.ObjectProperty
|
||||||
import scalafx.beans.property.StringProperty
|
import scalafx.beans.property.StringProperty
|
||||||
import scalafx.collections.ObservableBuffer
|
import scalafx.collections.ObservableBuffer
|
||||||
import scalafx.geometry.Insets
|
import scalafx.geometry.Insets
|
||||||
import scalafx.geometry.Pos
|
import scalafx.geometry.Pos
|
||||||
|
import scalafx.scene.Node
|
||||||
import scalafx.scene.Parent
|
import scalafx.scene.Parent
|
||||||
import scalafx.scene.Scene
|
import scalafx.scene.Scene
|
||||||
|
import scalafx.scene.control.Button
|
||||||
import scalafx.scene.control.Label
|
import scalafx.scene.control.Label
|
||||||
import scalafx.scene.control.TableColumn
|
import scalafx.scene.control.TableColumn
|
||||||
import scalafx.scene.control.TableView
|
import scalafx.scene.control.TableView
|
||||||
|
import scalafx.scene.control.Tooltip
|
||||||
import scalafx.scene.layout.BorderPane
|
import scalafx.scene.layout.BorderPane
|
||||||
import scalafx.scene.layout.HBox
|
import scalafx.scene.layout.HBox
|
||||||
import scalafx.scene.layout.Priority
|
import scalafx.scene.layout.Priority
|
||||||
import scalafx.scene.layout.StackPane
|
import scalafx.scene.layout.StackPane
|
||||||
|
import scalafx.util.Duration
|
||||||
class MainApp(
|
class MainApp(
|
||||||
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
||||||
schedulers: Schedulers,
|
schedulers: Schedulers,
|
||||||
startTime: Long
|
startTime: Long,
|
||||||
|
mediaPlayer: MediaPlayerResource
|
||||||
)(implicit logger: Logger[Task]) {
|
)(implicit logger: Logger[Task]) {
|
||||||
|
|
||||||
private lazy val _scene = new Scene {
|
private lazy val _scene = new Scene {
|
||||||
@ -60,18 +70,32 @@ class MainApp(
|
|||||||
private lazy val stage = new PrimaryStage {
|
private lazy val stage = new PrimaryStage {
|
||||||
title = "Simple ScalaFX App"
|
title = "Simple ScalaFX App"
|
||||||
scene = _scene
|
scene = _scene
|
||||||
width = 640
|
minWidth = 700
|
||||||
height = 480
|
minHeight = 520
|
||||||
|
width = 1280
|
||||||
|
height = 720
|
||||||
// resizable = false
|
// resizable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val program = for {
|
(for {
|
||||||
(fxApp, fxAppFib) <- wire[MyFxApp].init(stage)
|
(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
|
wire[MainAppDelegate].init
|
||||||
.flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
|
.flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
|
||||||
.executeOn(schedulers.fx)
|
.executeOn(schedulers.fx) >> Task.pure(stopSignal -> fxAppFib)
|
||||||
_ <- Task(stage.resizable = false).executeOn(schedulers.fx)
|
}
|
||||||
|
.use {
|
||||||
|
case (stopSignal, fxAppFib) =>
|
||||||
|
for {
|
||||||
|
// _ <- Task(stage.resizable = false).executeOn(schedulers.fx)
|
||||||
currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS)
|
currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS)
|
||||||
_ <- logger.info(
|
_ <- logger.info(
|
||||||
s"Application started in ${(currentTime - startTime) / 1000f} seconds"
|
s"Application started in ${(currentTime - startTime) / 1000f} seconds"
|
||||||
@ -79,65 +103,150 @@ class MainApp(
|
|||||||
// _ <- Task(CSSFX.start(stage))
|
// _ <- Task(CSSFX.start(stage))
|
||||||
_ <- fxAppFib.join
|
_ <- fxAppFib.join
|
||||||
} yield ()
|
} 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;
|
val buttonStyle = """| -fx-padding: 0.7em 0.57em;
|
||||||
| -fx-font-size: 14px;
|
| -fx-font-size: 14px;
|
||||||
| -jfx-button-type: RAISED;
|
| -jfx-button-type: RAISED;
|
||||||
| -fx-background-color: rgb(77,102,204);
|
| -fx-background-color: rgb(77,102,204);
|
||||||
| -fx-pref-width: 200;
|
| -fx-pref-width: 200;
|
||||||
| -fx-text-fill: WHITE; """.stripMargin
|
| -fx-text-fill: WHITE; """.stripMargin
|
||||||
|
val router = new FXRouter[Page]
|
||||||
|
|
||||||
val init =
|
// val players = mediaPlayerFactory.mediaPlayers
|
||||||
for {
|
// val videoPlayerController = players.newEmbeddedMediaPlayer()
|
||||||
//FXRouter does not allocate mutable state so it's ok to use pure here
|
// mediaPlayer.controller.videoSurface.set(
|
||||||
history <- Task(new MutHistory[Page](Page.Home))
|
// videoSurfaceForImageView(videoView)
|
||||||
router <- Task.pure(new FXRouter[Page](history))
|
// )
|
||||||
|
|
||||||
|
// 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)
|
routerStore <- router.store(Page.Home, logger)
|
||||||
todoStore <- TodoListStore(logger)
|
todoStore <- TodoListStore(logger)
|
||||||
todoComponent <- TodoListView(todoStore)
|
todoComponent <- TodoListView(todoStore)
|
||||||
resolver: PartialFunction[Page, Task[Parent]] = {
|
resolver: PartialFunction[Page, Parent] = {
|
||||||
case Page.Home =>
|
case Page.Home => videoPage
|
||||||
Task(new Label {
|
// 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")
|
styleClass ++= Seq("text-white")
|
||||||
text = "HomePage"
|
text = s"User Home, Id = ${Random.nextInt()}"
|
||||||
})
|
|
||||||
case Page.UserHome(id0) =>
|
|
||||||
Task(new Label {
|
|
||||||
styleClass ++= Seq("text-white")
|
|
||||||
text = s"User Home, Id = $id0"
|
|
||||||
})
|
|
||||||
case Page.Todo =>
|
|
||||||
Task.pure(todoComponent)
|
|
||||||
}
|
}
|
||||||
routerNode <-
|
case Page.Todo => todoComponent
|
||||||
|
}
|
||||||
|
routerNode: Node <-
|
||||||
Task
|
Task
|
||||||
.deferAction(implicit s =>
|
.deferAction(implicit s =>
|
||||||
Task(new HBox {
|
Task(new HBox { box =>
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
//TODO find a better way to do this
|
//TODO find a better way to do this
|
||||||
var oldValue: Option[Parent] = None
|
// videoView.fitWidth <== box.prefWidth
|
||||||
children <-- router
|
children <-- router
|
||||||
.render(resolver)(routerStore)
|
.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
|
// call cancel on the old component to cancel all subscriptions
|
||||||
.doOnNextF(newValue =>
|
.scan[Parent](new Label("empty")) { case (a, b) => b }
|
||||||
Coeval { oldValue.foreach(_ => ()) } >> Coeval {
|
.doOnNextF(s => logger.debug(s"Actual receive: $s"))
|
||||||
oldValue = Some(newValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.map(_.delegate)
|
.map(_.delegate)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mainSceneNode <- Task.deferAction(implicit s =>
|
mainSceneNode <- Task.deferAction(implicit s =>
|
||||||
Task(new StackPane { root =>
|
Task(new StackPane { root =>
|
||||||
|
alignment = Pos.Center
|
||||||
hgrow = Priority.Always
|
hgrow = Priority.Always
|
||||||
vgrow = Priority.Always
|
vgrow = Priority.Always
|
||||||
children = new BorderPane {
|
children = new BorderPane {
|
||||||
@ -145,28 +254,39 @@ class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
|||||||
vgrow = Priority.Always
|
vgrow = Priority.Always
|
||||||
center = routerNode
|
center = routerNode
|
||||||
bottom = new HBox {
|
bottom = new HBox {
|
||||||
|
implicit val cc = CompositeCancelable()
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
spacing = 20
|
spacing = 20
|
||||||
children = Seq(
|
children = Seq(
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
text = "Forward"
|
text = "Forward"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
onAction = () => {
|
obsAction.useLazyEval(
|
||||||
history.forward()
|
me.Task.pure(FXRouter.Forward)
|
||||||
routerStore.onNext(FXRouter.HistoryEvent(history.current))
|
) --> routerStore
|
||||||
|
disable <-- routerStore.map {
|
||||||
|
case (_, FXRouter.State(_, h)) =>
|
||||||
|
h.state.sp == h.state.values.size - 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
text = "Backward"
|
text = "Backward"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
onAction = () => {
|
|
||||||
history.backward()
|
obsAction.useLazyEval(
|
||||||
routerStore.onNext(FXRouter.HistoryEvent(history.current))
|
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 {
|
new JFXButton {
|
||||||
text = "Home"
|
text = "Home"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
|
disable <-- routerStore
|
||||||
|
.map { case (_, FXRouter.State(p, _)) => p === Page.Home }
|
||||||
obsAction
|
obsAction
|
||||||
.useLazyEval(
|
.useLazyEval(
|
||||||
me.Task.pure(FXRouter.Replace(Page.Home))
|
me.Task.pure(FXRouter.Replace(Page.Home))
|
||||||
@ -175,6 +295,8 @@ class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
|||||||
new JFXButton {
|
new JFXButton {
|
||||||
text = "Todo"
|
text = "Todo"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
|
disable <-- routerStore
|
||||||
|
.map { case (_, FXRouter.State(p, _)) => p === Page.Todo }
|
||||||
obsAction
|
obsAction
|
||||||
.useLazyEval(
|
.useLazyEval(
|
||||||
me.Task.pure(FXRouter.Replace(Page.Todo))
|
me.Task.pure(FXRouter.Replace(Page.Todo))
|
||||||
@ -183,22 +305,26 @@ class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
|||||||
new JFXButton {
|
new JFXButton {
|
||||||
text = "UserHome"
|
text = "UserHome"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
|
disable <-- routerStore
|
||||||
|
.map { case (_, FXRouter.State(p, _)) => p == Page.UserHome }
|
||||||
obsAction
|
obsAction
|
||||||
.useLazyEval(
|
.useLazyEval(
|
||||||
me.Task(
|
me.Task.pure(FXRouter.Replace(Page.UserHome))
|
||||||
FXRouter.Replace(Page.UserHome(Random.nextInt(20)))
|
|
||||||
)
|
|
||||||
) --> routerStore
|
) --> routerStore
|
||||||
},
|
},
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
text = "Dialog"
|
text = "Dialog"
|
||||||
style = buttonStyle
|
style = buttonStyle
|
||||||
val d = new JFXDialog()
|
val d = new JFXDialog {
|
||||||
d.setContent(new HBox {
|
content = new HBox {
|
||||||
children = Seq(new Label("hmm"))
|
style = "-fx-background-color: black"
|
||||||
padding = Insets(20)
|
children = Seq(new Label {
|
||||||
|
styleClass ++= Seq("text-white")
|
||||||
|
text = "Sample Dialog"
|
||||||
})
|
})
|
||||||
d.styleClass ++= Seq("text-white")
|
padding = Insets(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
onAction = () => d.show(root)
|
onAction = () => d.show(root)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -19,23 +19,23 @@ trait MainModule extends ActorModule with UiModule with HttpModule {
|
|||||||
"nova.monadic_sfx.util.reactive.store.Store" -> storeLogger
|
"nova.monadic_sfx.util.reactive.store.Store" -> storeLogger
|
||||||
)
|
)
|
||||||
.withFallback(defaultLogger)
|
.withFallback(defaultLogger)
|
||||||
.withAsync(timeWindow = 1.millis)
|
.withAsync(timeWindow = 10.millis)
|
||||||
|
|
||||||
def makeLogger =
|
def makeLogger =
|
||||||
for {
|
for {
|
||||||
defaultLogger <- consoleLogger[Task]()
|
defaultLogger <- consoleLogger[Task]()
|
||||||
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
|
.withAsync(timeWindow = 10.millis) |+| fileLogger[Task](
|
||||||
"application.log"
|
"application.log"
|
||||||
).withAsync(timeWindow = 1.millis)
|
).withAsync(timeWindow = 10.millis)
|
||||||
middlewareLogger <-
|
middlewareLogger <-
|
||||||
consoleLogger[
|
consoleLogger[
|
||||||
Task
|
Task
|
||||||
](formatter = Middlewares.format)
|
](formatter = Middlewares.format)
|
||||||
.withMinimalLevel(Level.Trace)
|
.withMinimalLevel(Level.Trace)
|
||||||
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
|
.withAsync(timeWindow = 10.millis) |+| fileLogger[Task](
|
||||||
"stores.log",
|
"stores.log",
|
||||||
formatter = Middlewares.format
|
formatter = Middlewares.format
|
||||||
).withAsync(timeWindow = 1.millis)
|
).withAsync(timeWindow = 10.millis)
|
||||||
routerLogger <- routerLogger(defaultLogger, middlewareLogger)
|
routerLogger <- routerLogger(defaultLogger, middlewareLogger)
|
||||||
} yield (routerLogger)
|
} yield (routerLogger)
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,7 @@ import sttp.client.httpclient.WebSocketHandler
|
|||||||
trait AppTypes {
|
trait AppTypes {
|
||||||
type HttpBackend =
|
type HttpBackend =
|
||||||
SttpBackend[Task, Observable[ByteBuffer], WebSocketHandler]
|
SttpBackend[Task, Observable[ByteBuffer], WebSocketHandler]
|
||||||
|
type CrossBackend =
|
||||||
|
SttpBackend[Task, Observable[ByteBuffer], Nothing]
|
||||||
}
|
}
|
||||||
object AppTypes extends AppTypes {}
|
object AppTypes extends AppTypes {}
|
||||||
|
@ -42,7 +42,7 @@ trait JavaFXMonixObservables {
|
|||||||
) =
|
) =
|
||||||
new ReadOnlyPropertyExt[T, J](prop)
|
new ReadOnlyPropertyExt[T, J](prop)
|
||||||
implicit def extendedObservableList[A](
|
implicit def extendedObservableList[A](
|
||||||
list: ObservableList[A]
|
list: ObservableBuffer[A]
|
||||||
) = new ObservableListExt(list)
|
) = new ObservableListExt(list)
|
||||||
implicit def extendedStringObservableList(
|
implicit def extendedStringObservableList(
|
||||||
list: ObservableList[String]
|
list: ObservableList[String]
|
||||||
@ -92,7 +92,7 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.onMouseDragged = l
|
scene.onMouseDragged = l;
|
||||||
c := Cancelable(() =>
|
c := Cancelable(() =>
|
||||||
scene.removeEventHandler(
|
scene.removeEventHandler(
|
||||||
jfxsi.MouseEvent.MOUSE_DRAGGED,
|
jfxsi.MouseEvent.MOUSE_DRAGGED,
|
||||||
@ -115,7 +115,7 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def <--(
|
def <--(
|
||||||
obs: Observable[_ <: T]
|
obs: Observable[T]
|
||||||
)(implicit s: Scheduler, c: CompositeCancelable): Unit = {
|
)(implicit s: Scheduler, c: CompositeCancelable): Unit = {
|
||||||
c += obs.doOnNextF(v => Coeval(prop.value = v)).subscribe()
|
c += obs.doOnNextF(v => Coeval(prop.value = v)).subscribe()
|
||||||
}
|
}
|
||||||
@ -142,7 +142,10 @@ object JavaFXMonixObservables {
|
|||||||
extends AnyVal {
|
extends AnyVal {
|
||||||
|
|
||||||
def -->(sub: Observer[A]) =
|
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]) = {
|
def ==>(op: Property[A, A]) = {
|
||||||
prop.onChange((a, b, c) => if (c != null) op() = c)
|
prop.onChange((a, b, c) => if (c != null) op() = c)
|
||||||
@ -168,7 +171,7 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ObservableListExt[A](
|
final class ObservableListExt[A](
|
||||||
private val buffer: ObservableList[A]
|
private val buffer: ObservableBuffer[A]
|
||||||
) extends AnyVal {
|
) extends AnyVal {
|
||||||
|
|
||||||
// def -->(sub: Observer[A]) =
|
// def -->(sub: Observer[A]) =
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
package nova.monadic_sfx
|
package nova.monadic_sfx
|
||||||
|
|
||||||
|
import monix.eval.TaskLike
|
||||||
|
import monix.reactive.Observable
|
||||||
|
|
||||||
package object implicits
|
package object implicits
|
||||||
extends MySfxObservableImplicits
|
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) {
|
// implicit class NodeExt(val node: Node) {
|
||||||
// def lookup2[T <: SFXDelegate[_]](
|
// 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 scala.concurrent.duration._
|
||||||
|
|
||||||
|
import cats.effect.Resource
|
||||||
import io.odin.Logger
|
import io.odin.Logger
|
||||||
|
import monix.bio.Fiber
|
||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
|
import monix.execution.CancelablePromise
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
import nova.monadic_sfx.executors.Schedulers
|
||||||
import nova.monadic_sfx.ui.DefaultUI
|
import nova.monadic_sfx.ui.DefaultUI
|
||||||
import scalafx.application.JFXApp
|
import scalafx.application.JFXApp3
|
||||||
import scalafx.application.JFXApp.PrimaryStage
|
import scalafx.application.JFXApp3.PrimaryStage
|
||||||
|
|
||||||
class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
|
||||||
|
|
||||||
private lazy val internal = new JFXApp {
|
private def internal(
|
||||||
|
startSignal: CancelablePromise[Unit],
|
||||||
|
stopSignal: CancelablePromise[Unit]
|
||||||
|
) =
|
||||||
|
new JFXApp3 {
|
||||||
|
def start(): Unit = {
|
||||||
stage = new PrimaryStage {
|
stage = new PrimaryStage {
|
||||||
scene = DefaultUI.scene
|
scene = DefaultUI.scene
|
||||||
}
|
}
|
||||||
|
startSignal.success(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// def stage = Task(internal.stage)
|
override def stopApp(): Unit = {
|
||||||
|
stopSignal.success(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// def stage_=(stage: PrimaryStage) = Task(internal.stage = stage)
|
}
|
||||||
|
|
||||||
// def useInternal[T](f: JFXApp => Task[T]): Task[T] =
|
object MyFxApp {
|
||||||
// for {
|
def resource(
|
||||||
// _ <- logger.debug("Request for using internal value")
|
schedulers: Schedulers,
|
||||||
// res <- f(internal).executeOn(schedulers.fx)
|
stage: => PrimaryStage,
|
||||||
// _ <- logger.debug(s"Result was ${res.toString()}")
|
transitionDelay: FiniteDuration = 500.millis
|
||||||
// } yield (res)
|
)(implicit
|
||||||
|
logger: Logger[Task]
|
||||||
def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) =
|
): Resource[Task, (Task[Unit], Fiber[Throwable, Unit])] =
|
||||||
for {
|
Resource.make(for {
|
||||||
_ <- logger.info("Starting FX App")
|
_ <- 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 <-
|
fib <-
|
||||||
Task(internal.main(Array.empty)).start.executeOn(schedulers.blocking)
|
Task(delegate.main(Array.empty)).start.executeOn(schedulers.blocking)
|
||||||
_ <- Task.sleep(200.millis)
|
_ <- Task.fromCancelablePromise(startSignal)
|
||||||
_ <- Task(internal.stage = stage)
|
_ <- Task.sleep(transitionDelay)
|
||||||
|
_ <- Task(delegate.stage = stage)
|
||||||
.executeOn(schedulers.fx)
|
.executeOn(schedulers.fx)
|
||||||
.delayExecution(delay)
|
.delayExecution(transitionDelay)
|
||||||
} yield (this, fib)
|
} 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 scala.concurrent.duration._
|
||||||
|
|
||||||
|
import cats.kernel.Eq
|
||||||
|
import cats.syntax.eq._
|
||||||
|
import com.softwaremill.quicklens._
|
||||||
import io.circe.Codec
|
import io.circe.Codec
|
||||||
import io.circe.Decoder
|
import io.circe.Decoder
|
||||||
import io.circe.Encoder
|
import io.circe.Encoder
|
||||||
@ -11,8 +14,8 @@ import io.odin.Logger
|
|||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
import monix.eval.Coeval
|
import monix.eval.Coeval
|
||||||
import monix.reactive.Observable
|
import monix.reactive.Observable
|
||||||
|
import nova.monadic_sfx.util.History
|
||||||
import nova.monadic_sfx.util.IOUtils
|
import nova.monadic_sfx.util.IOUtils
|
||||||
import nova.monadic_sfx.util.MutHistory
|
|
||||||
import nova.monadic_sfx.util.controls.JFXSpinner
|
import nova.monadic_sfx.util.controls.JFXSpinner
|
||||||
import nova.monadic_sfx.util.reactive.store.Middlewares
|
import nova.monadic_sfx.util.reactive.store.Middlewares
|
||||||
import nova.monadic_sfx.util.reactive.store.Reducer
|
import nova.monadic_sfx.util.reactive.store.Reducer
|
||||||
@ -21,7 +24,10 @@ import scalafx.scene.Parent
|
|||||||
|
|
||||||
object FXRouter {
|
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]
|
sealed abstract class Action[+T]
|
||||||
final case class Replace[T](page: T) extends Action[T]
|
final case class Replace[T](page: T) extends Action[T]
|
||||||
@ -31,16 +37,14 @@ object FXRouter {
|
|||||||
|
|
||||||
object Action {
|
object Action {
|
||||||
implicit def codec[T: Encoder: Decoder]: Codec[Action[T]] = deriveCodec
|
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]]
|
type FXStore[P] = Store[Action[P], State[P]]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FXRouter[P](history: MutHistory[P])(implicit
|
class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) {
|
||||||
E: Encoder[P],
|
|
||||||
D: Decoder[P]
|
|
||||||
) {
|
|
||||||
import FXRouter._
|
import FXRouter._
|
||||||
|
|
||||||
def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
|
def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
|
||||||
@ -52,9 +56,10 @@ class FXRouter[P](history: MutHistory[P])(implicit
|
|||||||
)
|
)
|
||||||
store <- Store.createL[Action[P], State[P]](
|
store <- Store.createL[Action[P], State[P]](
|
||||||
Replace(initialPage),
|
Replace(initialPage),
|
||||||
State(initialPage),
|
State(initialPage, History(initialPage)),
|
||||||
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _),
|
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _),
|
||||||
Seq(mw)
|
Seq(mw)
|
||||||
|
// Seq(classOf[HistoryEvent[P]])
|
||||||
)
|
)
|
||||||
} yield store
|
} yield store
|
||||||
)
|
)
|
||||||
@ -66,27 +71,39 @@ class FXRouter[P](history: MutHistory[P])(implicit
|
|||||||
action match {
|
action match {
|
||||||
// case Init => (state, None)
|
// case Init => (state, None)
|
||||||
case Replace(p) =>
|
case Replace(p) =>
|
||||||
history.push(p)
|
(state.copy(page = p, history = state.history :+ p), None)
|
||||||
(state.copy(page = p), None)
|
|
||||||
case HistoryEvent(p) =>
|
case HistoryEvent(p) =>
|
||||||
(state.copy(page = p), None)
|
(state.copy(page = p), None)
|
||||||
case Forward => (state, None)
|
case Forward =>
|
||||||
case Backward => (state, None)
|
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(
|
def render(
|
||||||
resolver: P => Task[Parent],
|
resolver: P => Parent,
|
||||||
transitionDelay: FiniteDuration = 500.millis
|
transitionDelay: FiniteDuration = 500.millis
|
||||||
)(implicit store: FXStore[P]) =
|
)(implicit store: FXStore[P]) =
|
||||||
store
|
store
|
||||||
|
.filter {
|
||||||
|
case (a, _) => a =!= FXRouter.Forward
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
case (a, _) => a =!= FXRouter.Backward
|
||||||
|
}
|
||||||
|
.distinctUntilChanged
|
||||||
.flatMap {
|
.flatMap {
|
||||||
case (_, FXRouter.State(p)) =>
|
case (_, FXRouter.State(p, _)) =>
|
||||||
Observable.from(Coeval(new JFXSpinner)) ++ Observable.from(
|
Observable.from(Coeval(new JFXSpinner)) ++ Observable.from(
|
||||||
IOUtils.toTask(
|
IOUtils.toTask(
|
||||||
Task
|
Task
|
||||||
.racePair(
|
.racePair(
|
||||||
Task.sleep(transitionDelay),
|
Task.sleep(transitionDelay),
|
||||||
resolver(p)
|
Task.pure(resolver(p))
|
||||||
)
|
)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
case Left(_ -> fib) => fib.join
|
case Left(_ -> fib) => fib.join
|
||||||
@ -108,8 +125,10 @@ class FXRouter[P](history: MutHistory[P])(implicit
|
|||||||
sealed trait Page
|
sealed trait Page
|
||||||
object Page {
|
object Page {
|
||||||
final case object Home extends 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
|
final case object Todo extends Page
|
||||||
|
|
||||||
|
implicit val eqForPage = Eq.fromUniversalEquals[Page]
|
||||||
}
|
}
|
||||||
|
|
||||||
// case class State()
|
// case class State()
|
||||||
|
@ -8,12 +8,10 @@ import monix.bio.Task
|
|||||||
import monix.catnap.ConcurrentChannel
|
import monix.catnap.ConcurrentChannel
|
||||||
import monix.catnap.ConsumerF
|
import monix.catnap.ConsumerF
|
||||||
import monix.execution.Scheduler
|
import monix.execution.Scheduler
|
||||||
import monix.reactive.Observable
|
|
||||||
import monix.reactive.Observer
|
import monix.reactive.Observer
|
||||||
import nova.monadic_sfx.util.controls.FontIcon
|
import nova.monadic_sfx.util.controls.FontIcon
|
||||||
import nova.monadic_sfx.util.controls.IconLiteral
|
import nova.monadic_sfx.util.controls.IconLiteral
|
||||||
import nova.monadic_sfx.util.controls.JFXListView
|
import nova.monadic_sfx.util.controls.JFXListView
|
||||||
import nova.monadic_sfx.implicits._
|
|
||||||
import nova.monadic_sfx.util.reactive.store._
|
import nova.monadic_sfx.util.reactive.store._
|
||||||
import scalafx.Includes._
|
import scalafx.Includes._
|
||||||
import scalafx.beans.property.StringProperty
|
import scalafx.beans.property.StringProperty
|
||||||
@ -57,13 +55,12 @@ object TodoListViewOld {
|
|||||||
): Task[JFXListView[Todo]] =
|
): Task[JFXListView[Todo]] =
|
||||||
Task.deferAction(implicit s =>
|
Task.deferAction(implicit s =>
|
||||||
Task {
|
Task {
|
||||||
val todos =
|
val todos = store.map { case (_, items) => items }
|
||||||
store.map { case (_, items) => items }
|
|
||||||
|
|
||||||
val listView = new JFXListView[Todo] { lv =>
|
val listView = new JFXListView[Todo] { lv =>
|
||||||
def selectedItems = lv.selectionModel().selectedItems.view
|
def selectedItems = lv.selectionModel().selectedItems.view
|
||||||
// items = todos
|
// items = todos
|
||||||
items <-- todos
|
// items <-- todos
|
||||||
// .map(ObservableBuffer.from(_))
|
// .map(ObservableBuffer.from(_))
|
||||||
cellFactory = _ =>
|
cellFactory = _ =>
|
||||||
new ListCell[Todo] {
|
new ListCell[Todo] {
|
||||||
@ -216,14 +213,14 @@ object TodoListComponentOld {
|
|||||||
case _ => state
|
case _ => state
|
||||||
}
|
}
|
||||||
|
|
||||||
val store =
|
// val store =
|
||||||
Store
|
// Store
|
||||||
.createL[TodoListComponentOld.Command, Vector[Todo]](
|
// .createL[TodoListComponentOld.Command, Vector[Todo]](
|
||||||
TodoListComponentOld.Delete(0),
|
// TodoListComponentOld.Delete(0),
|
||||||
Vector.empty[Todo],
|
// Vector.empty[Todo],
|
||||||
(s: Vector[Todo], a: TodoListComponentOld.Command) =>
|
// (s: Vector[Todo], a: TodoListComponentOld.Command) =>
|
||||||
reducer(s, a) -> Observable.empty
|
// reducer(s, a) -> Observable.empty
|
||||||
)
|
// )
|
||||||
|
|
||||||
class Props(
|
class Props(
|
||||||
val todoListView: TodoListViewOld,
|
val todoListView: TodoListViewOld,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package nova.monadic_sfx.ui.components.todo
|
package nova.monadic_sfx.ui.components.todo
|
||||||
|
|
||||||
|
import cats.kernel.Eq
|
||||||
import com.softwaremill.quicklens._
|
import com.softwaremill.quicklens._
|
||||||
import io.circe.generic.JsonCodec
|
import io.circe.generic.JsonCodec
|
||||||
import io.odin.Logger
|
import io.odin.Logger
|
||||||
@ -9,6 +10,9 @@ import nova.monadic_sfx.util.reactive.store.Reducer
|
|||||||
import nova.monadic_sfx.util.reactive.store.Store
|
import nova.monadic_sfx.util.reactive.store.Store
|
||||||
|
|
||||||
case class Todo(id: Int, content: String)
|
case class Todo(id: Int, content: String)
|
||||||
|
object Todo {
|
||||||
|
implicit val eqForTodo = Eq.fromUniversalEquals[Todo]
|
||||||
|
}
|
||||||
|
|
||||||
object TodoListStore {
|
object TodoListStore {
|
||||||
|
|
||||||
@ -21,8 +25,15 @@ object TodoListStore {
|
|||||||
|
|
||||||
private case class InternalAdd(content: String) extends Action
|
private case class InternalAdd(content: String) extends Action
|
||||||
private case object End extends Action
|
private case object End extends Action
|
||||||
|
object Action {
|
||||||
|
implicit val eqForAction = Eq.fromUniversalEquals[Action]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
case class State(todos: Vector[Todo], counter: Int)
|
case class State(todos: Vector[Todo], counter: Int)
|
||||||
|
object State {
|
||||||
|
implicit val eqForState = Eq.fromUniversalEquals[State]
|
||||||
|
}
|
||||||
|
|
||||||
def reducer(logger: Logger[Task])(
|
def reducer(logger: Logger[Task])(
|
||||||
state: State,
|
state: State,
|
||||||
@ -32,12 +43,13 @@ object TodoListStore {
|
|||||||
case Init => (state, None)
|
case Init => (state, None)
|
||||||
case Add(content) =>
|
case Add(content) =>
|
||||||
val nextAction = Some(for {
|
val nextAction = Some(for {
|
||||||
|
// do some validation
|
||||||
// _ <- logger.debug(s"Received $content")
|
// _ <- logger.debug(s"Received $content")
|
||||||
res <- Task.pure(InternalAdd(content))
|
res <- Task.pure(InternalAdd(content))
|
||||||
} yield res)
|
} yield res)
|
||||||
(state, nextAction)
|
(state, nextAction)
|
||||||
case Edit(_id, content) =>
|
case Edit(id, content) =>
|
||||||
val condition: Todo => Boolean = _.id == _id
|
val condition: Todo => Boolean = _.id == id
|
||||||
val nextState = state
|
val nextState = state
|
||||||
.modify(_.todos.eachWhere(condition))
|
.modify(_.todos.eachWhere(condition))
|
||||||
.using(_.copy(content = content))
|
.using(_.copy(content = content))
|
||||||
@ -46,15 +58,17 @@ object TodoListStore {
|
|||||||
(state.copy(state.todos.filterNot(_.id == id)), None)
|
(state.copy(state.todos.filterNot(_.id == id)), None)
|
||||||
|
|
||||||
case InternalAdd(content) =>
|
case InternalAdd(content) =>
|
||||||
val nextState = state.copy(
|
val nextState =
|
||||||
todos = state.todos :+ Todo(state.counter, content),
|
state
|
||||||
counter = state.counter + 1
|
.modify(_.todos)
|
||||||
)
|
.using(_ :+ Todo(state.counter, content))
|
||||||
|
.modify(_.counter)
|
||||||
|
.using(_ + 1)
|
||||||
(nextState, Some(logger.debug(s"Received $content") >> Task.pure(End)))
|
(nextState, Some(logger.debug(s"Received $content") >> Task.pure(End)))
|
||||||
case End => (state, None)
|
case End => (state, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply(logger: Logger[Task]) =
|
def apply(logger: Logger[Task]): Task[Store[Action, State]] =
|
||||||
Task.deferAction(implicit s =>
|
Task.deferAction(implicit s =>
|
||||||
for {
|
for {
|
||||||
logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore")
|
logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore")
|
||||||
@ -64,12 +78,9 @@ object TodoListStore {
|
|||||||
Init,
|
Init,
|
||||||
State(Vector.empty[Todo], 0),
|
State(Vector.empty[Todo], 0),
|
||||||
Reducer.withOptionalEffects(reducer(logger) _),
|
Reducer.withOptionalEffects(reducer(logger) _),
|
||||||
Seq(
|
Seq(logMware)
|
||||||
// actionLoggerMiddleware(logger, "TodoStore2")
|
|
||||||
logMware
|
|
||||||
)
|
)
|
||||||
)
|
} yield store
|
||||||
} yield (store)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
package nova.monadic_sfx.ui.components.todo
|
package nova.monadic_sfx.ui.components.todo
|
||||||
|
|
||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
|
import monix.eval.Coeval
|
||||||
import monix.execution.cancelables.CompositeCancelable
|
import monix.execution.cancelables.CompositeCancelable
|
||||||
import monix.{eval => me}
|
import monix.{eval => me}
|
||||||
|
import nova.monadic_sfx.implicits._
|
||||||
import nova.monadic_sfx.util.controls.FontIcon
|
import nova.monadic_sfx.util.controls.FontIcon
|
||||||
import nova.monadic_sfx.util.controls.IconLiteral
|
import nova.monadic_sfx.util.controls.IconLiteral
|
||||||
import nova.monadic_sfx.util.controls.JFXButton
|
import nova.monadic_sfx.util.controls.JFXButton
|
||||||
import nova.monadic_sfx.util.controls.JFXListView
|
import nova.monadic_sfx.util.controls.JFXListView
|
||||||
import nova.monadic_sfx.util.controls.JFXTextField
|
import nova.monadic_sfx.util.controls.JFXTextField
|
||||||
import nova.monadic_sfx.util.controls.MenuItem
|
import nova.monadic_sfx.util.controls.MenuItem
|
||||||
import nova.monadic_sfx.implicits._
|
|
||||||
import nova.monadic_sfx.util.reactive.store._
|
import nova.monadic_sfx.util.reactive.store._
|
||||||
import org.gerweck.scalafx.util._
|
import org.gerweck.scalafx.util._
|
||||||
import scalafx.Includes._
|
import scalafx.Includes._
|
||||||
import scalafx.beans.property.ObjectProperty
|
import scalafx.beans.property.ObjectProperty
|
||||||
import scalafx.beans.property.StringProperty
|
import scalafx.beans.property.StringProperty
|
||||||
|
import scalafx.collections.ObservableBuffer
|
||||||
import scalafx.geometry.Insets
|
import scalafx.geometry.Insets
|
||||||
|
import scalafx.geometry.Pos
|
||||||
import scalafx.scene.Parent
|
import scalafx.scene.Parent
|
||||||
import scalafx.scene.control.ContextMenu
|
import scalafx.scene.control.ContextMenu
|
||||||
|
import scalafx.scene.control.Label
|
||||||
import scalafx.scene.control.ListCell
|
import scalafx.scene.control.ListCell
|
||||||
import scalafx.scene.control.SelectionMode
|
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.BorderPane
|
||||||
|
import scalafx.scene.layout.HBox
|
||||||
import scalafx.scene.layout.Priority
|
import scalafx.scene.layout.Priority
|
||||||
|
import scalafx.scene.paint.Color
|
||||||
|
|
||||||
object TodoListView {
|
object TodoListView {
|
||||||
def apply(
|
def apply(
|
||||||
@ -32,23 +35,29 @@ object TodoListView {
|
|||||||
): Task[Parent] =
|
): Task[Parent] =
|
||||||
Task.deferAction(implicit s =>
|
Task.deferAction(implicit s =>
|
||||||
Task {
|
Task {
|
||||||
val cc = CompositeCancelable()
|
implicit val cc = CompositeCancelable()
|
||||||
val todos = store.map { case (_, state) => state.todos }
|
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])
|
val _selectedItems = ObjectProperty(Seq.empty[Todo])
|
||||||
|
|
||||||
new BorderPane {
|
new BorderPane {
|
||||||
padding = Insets(5)
|
padding = Insets(5)
|
||||||
|
hgrow = Priority.Always
|
||||||
val _content = StringProperty("")
|
val _content = StringProperty("")
|
||||||
center = new HBox {
|
center = new HBox {
|
||||||
padding = Insets(5)
|
padding = Insets(5)
|
||||||
children ++= Seq(new JFXListView[Todo] {
|
children ++= Seq(new JFXListView[Todo] {
|
||||||
|
id = "todoList"
|
||||||
hgrow = Priority.Always
|
hgrow = Priority.Always
|
||||||
def selectedItems = selectionModel().selectedItems.view
|
def selectedItems = selectionModel().selectedItems.view
|
||||||
styleClass ++= Seq("text-white")
|
styleClass ++= Seq("text-white", "clear-list-view")
|
||||||
selectionModel().selectionMode = SelectionMode.Multiple
|
selectionModel().selectionMode = SelectionMode.Multiple
|
||||||
selectionModel().selectedItems.observableSeqValue ==> _selectedItems
|
selectionModel().selectedItems.observableSeqValue ==> _selectedItems
|
||||||
|
|
||||||
cc += items <-- todos
|
items <-- todos.map(_.delegate)
|
||||||
|
|
||||||
val emptyCell = ObjectProperty(new HBox)
|
val emptyCell = ObjectProperty(new HBox)
|
||||||
cellFactory = _ =>
|
cellFactory = _ =>
|
||||||
@ -56,12 +65,16 @@ object TodoListView {
|
|||||||
val _text = StringProperty("")
|
val _text = StringProperty("")
|
||||||
val _graphic = ObjectProperty(
|
val _graphic = ObjectProperty(
|
||||||
new HBox {
|
new HBox {
|
||||||
|
styleClass ++= Seq("text-white", "strong", "todo-cell")
|
||||||
children = Seq(
|
children = Seq(
|
||||||
new FontIcon {
|
new FontIcon {
|
||||||
iconSize = 10
|
iconSize = 20
|
||||||
iconLiteral = IconLiteral.Gmi10k
|
iconLiteral = IconLiteral.Gmi10k
|
||||||
|
fill = Color.White
|
||||||
},
|
},
|
||||||
new Text {
|
new Label {
|
||||||
|
style = "-fx-text-fill: white "
|
||||||
|
styleClass ++= Seq("text-white", "strong")
|
||||||
text <== _text
|
text <== _text
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -100,6 +113,7 @@ object TodoListView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bottom = new HBox {
|
bottom = new HBox {
|
||||||
@ -107,22 +121,29 @@ object TodoListView {
|
|||||||
padding = Insets(5)
|
padding = Insets(5)
|
||||||
children = Seq(
|
children = Seq(
|
||||||
new JFXTextField {
|
new JFXTextField {
|
||||||
|
id = "todoInputField"
|
||||||
|
style = "-fx-background-color: rgb(38,38,38);"
|
||||||
|
styleClass += "text-white"
|
||||||
text ==> _content
|
text ==> _content
|
||||||
|
vgrow = Priority.Always
|
||||||
},
|
},
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
|
id = "todoAddButton"
|
||||||
text = "Add"
|
text = "Add"
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
// disable <== _selectedItems.map(_.length > 0)
|
// disable <== _selectedItems.map(_.length > 0)
|
||||||
styleClass = Seq("btn", "btn-primary")
|
styleClass = Seq("btn", "btn-primary")
|
||||||
obsAction
|
obsAction
|
||||||
.useLazyEval(me.Task(TodoListStore.Add(_content()))) --> store
|
.useLazyEval(
|
||||||
|
me.Task(TodoListStore.Add(_content()))
|
||||||
|
) --> store
|
||||||
},
|
},
|
||||||
new JFXButton {
|
new JFXButton {
|
||||||
|
id = "todoEditButton"
|
||||||
text = "Edit"
|
text = "Edit"
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
disable <== _selectedItems.map(_.length > 1)
|
disable <== _selectedItems.map(_.length > 1)
|
||||||
styleClass = Seq("btn", "btn-info")
|
styleClass = Seq("btn", "btn-info")
|
||||||
style = ""
|
|
||||||
obsAction.useLazyEval(
|
obsAction.useLazyEval(
|
||||||
me.Task(
|
me.Task(
|
||||||
TodoListStore.Edit(
|
TodoListStore.Edit(
|
||||||
|
@ -6,13 +6,13 @@ import animatefx.animation.FadeIn
|
|||||||
import animatefx.util.{SequentialAnimationFX => SeqFX}
|
import animatefx.util.{SequentialAnimationFX => SeqFX}
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
import monix.eval.Task
|
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.FontIcon
|
||||||
import nova.monadic_sfx.util.controls.IconLiteral
|
import nova.monadic_sfx.util.controls.IconLiteral
|
||||||
import nova.monadic_sfx.util.controls.JFXButton
|
import nova.monadic_sfx.util.controls.JFXButton
|
||||||
import nova.monadic_sfx.util.controls.JFXListView
|
import nova.monadic_sfx.util.controls.JFXListView
|
||||||
import nova.monadic_sfx.util.controls.JFXTextArea
|
import nova.monadic_sfx.util.controls.JFXTextArea
|
||||||
import nova.monadic_sfx.util.controls.JFXTextField
|
import nova.monadic_sfx.util.controls.JFXTextField
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponentOld
|
|
||||||
import scalafx.collections.ObservableBuffer
|
import scalafx.collections.ObservableBuffer
|
||||||
import scalafx.scene.control.Label
|
import scalafx.scene.control.Label
|
||||||
import scalafx.scene.layout.HBox
|
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
|
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)
|
private var _values = Vector(initValue)
|
||||||
def values = _values
|
def values = _values
|
||||||
private var _sp = 0
|
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 com.jfoenix.{controls => jfoenixc}
|
||||||
import javafx.{scene => jfxs}
|
import javafx.{scene => jfxs}
|
||||||
|
import nova.monadic_sfx.implicits._
|
||||||
import scalafx.Includes._
|
import scalafx.Includes._
|
||||||
import scalafx.beans.property.ObjectProperty
|
import scalafx.beans.property.ObjectProperty
|
||||||
import scalafx.scene.Node
|
import scalafx.scene.Node
|
||||||
import scalafx.scene.control.Button
|
import scalafx.scene.control.Button
|
||||||
import nova.monadic_sfx.implicits._
|
|
||||||
|
|
||||||
import jfxs.{paint => jfxsp}
|
import jfxs.{paint => jfxsp}
|
||||||
|
|
||||||
@ -30,14 +30,10 @@ class JFXButton(
|
|||||||
def this(text: String, graphic: Node) =
|
def this(text: String, graphic: Node) =
|
||||||
this(new jfoenixc.JFXButton(text, graphic))
|
this(new jfoenixc.JFXButton(text, graphic))
|
||||||
|
|
||||||
def ripplerFill: ObjectProperty[jfxsp.Paint] =
|
def ripplerFill: ObjectProperty[jfxsp.Paint] = delegate.ripplerFillProperty
|
||||||
jfxObjectProperty2sfx(delegate.ripplerFillProperty)
|
|
||||||
|
|
||||||
def ripplerFill_=(b: jfxsp.Paint): Unit = {
|
def ripplerFill_=(b: jfxsp.Paint): Unit = ripplerFill() = b
|
||||||
ripplerFill() = b
|
|
||||||
}
|
|
||||||
|
|
||||||
def obsAction =
|
def obsAction = new ActionObservableBuilder(this.observableAction)
|
||||||
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.beans.property.ReadOnlyObjectProperty
|
||||||
import scalafx.delegate.SFXDelegate
|
import scalafx.delegate.SFXDelegate
|
||||||
import scalafx.scene.control.IndexedCell
|
import scalafx.scene.control.IndexedCell
|
||||||
|
import scalafx.scene.control.ListCell
|
||||||
import scalafx.scene.control.ListView
|
import scalafx.scene.control.ListView
|
||||||
|
|
||||||
object JFXListCell {
|
object JFXListCell {
|
||||||
implicit def sfxListCell2jfx[T](
|
implicit def sfxListCell2jfx[T](
|
||||||
l: JFXListCell[T]
|
l: JFXListCell[T]
|
||||||
): jfoenixc.JFXListCell[T] =
|
): ListCell[T] =
|
||||||
if (l != null) l.delegate else null
|
if (l != null) l.delegate else null
|
||||||
}
|
}
|
||||||
|
|
||||||
class JFXListCell[T](
|
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)
|
) extends IndexedCell(delegate)
|
||||||
with SFXDelegate[jfoenixc.JFXListCell[T]] {
|
with SFXDelegate[jfoenixc.JFXListCell[T]] {
|
||||||
|
|
||||||
@ -33,4 +50,6 @@ class JFXListCell[T](
|
|||||||
delegate.updateListView(listView)
|
delegate.updateListView(listView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delegate.cell
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,16 +25,8 @@ class JFXListView[T](
|
|||||||
// v.foreach { items() = _ }
|
// v.foreach { items() = _ }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
def items_=(
|
def items_=(v: Observable[Seq[T]])(implicit s: Scheduler): Unit = {
|
||||||
v: Observable[Seq[T]]
|
v.map(ObservableBuffer.from).foreach(items() = _)
|
||||||
)(implicit s: Scheduler): Unit = {
|
|
||||||
v
|
|
||||||
.map {
|
|
||||||
// case buf: ObservableBuffer[T] => buf
|
|
||||||
case other => ObservableBuffer.from(other)
|
|
||||||
}
|
|
||||||
// .map(myDiff(items(), _))
|
|
||||||
.foreach { items() = _ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def depth = delegate.depthProperty()
|
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 io.odin.meta.Render
|
||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
import monix.reactive.Observable
|
import monix.reactive.Observable
|
||||||
// object Middleware {
|
|
||||||
// def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob
|
|
||||||
// }
|
|
||||||
|
|
||||||
@JsonCodec
|
@JsonCodec
|
||||||
final case class StoreInfo[A](
|
final case class StoreInfo[A](
|
||||||
@ -34,19 +31,12 @@ object StoreInfo {
|
|||||||
|
|
||||||
object Middlewares {
|
object Middlewares {
|
||||||
|
|
||||||
// val encoder: Encoder[LoggerMessage] =
|
|
||||||
// Encoder.forProduct1("message")(m => m.message.value)
|
|
||||||
|
|
||||||
val format = create(ThrowableFormat.Default, PositionFormat.Full)
|
val format = create(ThrowableFormat.Default, PositionFormat.Full)
|
||||||
|
|
||||||
def create(
|
def create(
|
||||||
throwableFormat: ThrowableFormat,
|
throwableFormat: ThrowableFormat,
|
||||||
positionFormat: PositionFormat
|
positionFormat: PositionFormat
|
||||||
): io.odin.formatter.Formatter = {
|
): io.odin.formatter.Formatter = (msg: LoggerMessage) => msg.message.value
|
||||||
// val encoder: Encoder[LoggerMessage] =
|
|
||||||
// Encoder.forProduct1("message")(m => m.message.value)
|
|
||||||
(msg: LoggerMessage) => msg.message.value
|
|
||||||
}
|
|
||||||
|
|
||||||
def actionStateLoggerMiddleware[A, M](
|
def actionStateLoggerMiddleware[A, M](
|
||||||
logger: Logger[Task]
|
logger: Logger[Task]
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
package nova.monadic_sfx.util.reactive.store
|
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.bio.Task
|
||||||
import monix.eval.Coeval
|
import monix.eval.Coeval
|
||||||
import monix.reactive.Observable
|
import monix.reactive.Observer
|
||||||
import monix.reactive.OverflowStrategy
|
import monix.reactive.OverflowStrategy
|
||||||
import monix.reactive.subjects.ConcurrentSubject
|
import monix.reactive.subjects.ConcurrentSubject
|
||||||
|
|
||||||
@ -34,18 +30,22 @@ object Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val obs = Observable.suspend(
|
val obs = subject
|
||||||
subject
|
|
||||||
.scanEval0F[Coeval, (A, M)](
|
.scanEval0F[Coeval, (A, M)](
|
||||||
Coeval.pure(initialAction -> initialState)
|
Coeval.pure(initialAction -> initialState)
|
||||||
)(fold)
|
)(fold)
|
||||||
.behavior(initialAction -> initialState)
|
|
||||||
.refCount
|
|
||||||
)
|
|
||||||
|
|
||||||
val res = middlewares.foldLeft(obs) {
|
val res = middlewares
|
||||||
|
.foldLeft(obs) {
|
||||||
case (obs, middleware) => middleware(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(
|
MonixProSubject.from(
|
||||||
subject,
|
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