Cleanup and refactoring
This commit is contained in:
parent
b988ad267e
commit
8f1fe0cc84
@ -25,6 +25,7 @@ libraryDependencies ++= Seq(
|
|||||||
"com.softwaremill.sttp.client" %% "circe" % "2.2.9",
|
"com.softwaremill.sttp.client" %% "circe" % "2.2.9",
|
||||||
"com.softwaremill.sttp.client" %% "async-http-client-backend-monix" % "2.2.9",
|
"com.softwaremill.sttp.client" %% "async-http-client-backend-monix" % "2.2.9",
|
||||||
"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.github.valskalla" %% "odin-monix" % "0.8.1",
|
"com.github.valskalla" %% "odin-monix" % "0.8.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",
|
||||||
|
@ -23,10 +23,10 @@ object Main extends MainModule with BIOApp {
|
|||||||
"application.log"
|
"application.log"
|
||||||
).withAsync()
|
).withAsync()
|
||||||
schedulers = new Schedulers()
|
schedulers = new Schedulers()
|
||||||
backend <- Resource.make(
|
// backend <- Resource.make(
|
||||||
toIO(HttpClientMonixBackend()(schedulers.async))
|
// toIO(HttpClientMonixBackend()(schedulers.async))
|
||||||
)(c => toIO(c.close()))
|
// )(c => toIO(c.close()))
|
||||||
actorSystem <- actorSystemResource(logger)
|
// actorSystem <- actorSystemResource(logger)
|
||||||
_ <- Resource.liftF(wire[MainApp].program)
|
_ <- Resource.liftF(wire[MainApp].program)
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
|
@ -3,15 +3,11 @@ package nova.monadic_sfx
|
|||||||
import com.softwaremill.macwire._
|
import com.softwaremill.macwire._
|
||||||
import io.odin.Logger
|
import io.odin.Logger
|
||||||
import monix.bio.Task
|
import monix.bio.Task
|
||||||
import monix.catnap.ConcurrentChannel
|
|
||||||
import nova.monadic_sfx.executors.Schedulers
|
import nova.monadic_sfx.executors.Schedulers
|
||||||
import nova.monadic_sfx.implicits.JFXButton
|
import nova.monadic_sfx.implicits.JFXButton
|
||||||
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
||||||
import nova.monadic_sfx.ui.MyFxApp
|
import nova.monadic_sfx.ui.MyFxApp
|
||||||
import nova.monadic_sfx.ui.components.todo.Todo
|
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent
|
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListView
|
import nova.monadic_sfx.ui.components.todo.TodoListView
|
||||||
import nova.monadic_sfx.util.IOUtils._
|
|
||||||
import org.gerweck.scalafx.util._
|
import org.gerweck.scalafx.util._
|
||||||
import scalafx.Includes._
|
import scalafx.Includes._
|
||||||
import scalafx.application.JFXApp.PrimaryStage
|
import scalafx.application.JFXApp.PrimaryStage
|
||||||
@ -25,6 +21,7 @@ import scalafx.scene.control.TableView
|
|||||||
import scalafx.scene.layout.HBox
|
import scalafx.scene.layout.HBox
|
||||||
import scalafx.scene.paint.Color
|
import scalafx.scene.paint.Color
|
||||||
import scalafx.scene.shape.Rectangle
|
import scalafx.scene.shape.Rectangle
|
||||||
|
import nova.monadic_sfx.ui.components.todo.TodoListStore
|
||||||
|
|
||||||
class MainApp(
|
class MainApp(
|
||||||
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
||||||
@ -38,7 +35,7 @@ class MainApp(
|
|||||||
|
|
||||||
lazy val addTodoObs = addTodoButton.observableAction()
|
lazy val addTodoObs = addTodoButton.observableAction()
|
||||||
|
|
||||||
lazy val todoListView = TodoListView.defaultListView
|
// lazy val todoListView = TodoListView.defaultListView
|
||||||
|
|
||||||
lazy val _scene = new Scene {
|
lazy val _scene = new Scene {
|
||||||
root = new HBox {
|
root = new HBox {
|
||||||
@ -77,59 +74,66 @@ class MainApp(
|
|||||||
// _ <- Task(fxApp.stage = stage)
|
// _ <- Task(fxApp.stage = stage)
|
||||||
// .executeOn(schedulers.fx)
|
// .executeOn(schedulers.fx)
|
||||||
// .delayExecution(2000.millis)
|
// .delayExecution(2000.millis)
|
||||||
todoComponent <- createTodoComponent
|
// todoComponent <- createTodoComponent
|
||||||
_ <- toIO(
|
// _ <- toIO(
|
||||||
addTodoObs
|
// addTodoObs
|
||||||
.mapEval(_ =>
|
// .mapEval(_ =>
|
||||||
toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
|
// toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
|
||||||
)
|
// )
|
||||||
.completedL
|
// .completedL
|
||||||
.executeOn(schedulers.fx)
|
// .executeOn(schedulers.fx)
|
||||||
.startAndForget
|
// .startAndForget
|
||||||
)
|
// )
|
||||||
|
_ <- createTodoComponent.executeOn(schedulers.fx)
|
||||||
_ <- logger.info(
|
_ <- logger.info(
|
||||||
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds"
|
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds"
|
||||||
)
|
)
|
||||||
_ <- fxAppFib.join
|
_ <- fxAppFib.join
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
def createTodoComponent: Task[TodoListComponent] = {
|
// def createTodoComponent: Task[TodoListComponent] = {
|
||||||
|
// for {
|
||||||
|
// channel <-
|
||||||
|
// ConcurrentChannel
|
||||||
|
// .of[Task, TodoListComponent.Complete, TodoListComponent.Command]
|
||||||
|
// scheduler = schedulers.fx
|
||||||
|
// lv <- TodoListView.defaultListView2.executeOn(scheduler)
|
||||||
|
// // todoLV = new TodoListView(lv)
|
||||||
|
// todoComponent <- wire[TodoListComponent.Props].create
|
||||||
|
// // TODO make this a "message pass" instead of mutating directly
|
||||||
|
// _ <- Task(_scene.getChildren += lv).executeOn(scheduler)
|
||||||
|
// // _ <- toIO(
|
||||||
|
// // delObs
|
||||||
|
// // .doOnNext(_ => toTask(logger.debug("Pressed delete")))
|
||||||
|
// // .doOnNext(todo =>
|
||||||
|
// // toTask(
|
||||||
|
// // for {
|
||||||
|
// // _ <- logger.debug(s"Got todo $todo")
|
||||||
|
// // _ <- todoComponent.send(TodoListComponent.Delete(todo.id))
|
||||||
|
// // // _ <- Task.sequence(
|
||||||
|
// // // lst.map(todo =>
|
||||||
|
// // // todoComponent.send(TodoListComponent.Delete(todo.id))
|
||||||
|
// // // )
|
||||||
|
// // // )
|
||||||
|
// // } yield ()
|
||||||
|
// // )
|
||||||
|
// // )
|
||||||
|
// // .completedL
|
||||||
|
// // ).startAndForget
|
||||||
|
// // _ <- toIO(
|
||||||
|
// // editObs
|
||||||
|
// // .doOnNext(_ => toTask(logger.debug("Pressed edit")))
|
||||||
|
// // .completedL
|
||||||
|
// // ).startAndForget
|
||||||
|
// } yield todoComponent
|
||||||
|
// }
|
||||||
|
|
||||||
|
def createTodoComponent: Task[Unit] =
|
||||||
for {
|
for {
|
||||||
channel <-
|
store <- TodoListStore(logger)
|
||||||
ConcurrentChannel
|
lv <- TodoListView(store)
|
||||||
.of[Task, TodoListComponent.Complete, TodoListComponent.Command]
|
_ <- Task(_scene.getChildren += lv)
|
||||||
scheduler = schedulers.fx
|
} yield ()
|
||||||
(lv, delObs, editObs) <-
|
|
||||||
TodoListView.defaultListView2.executeOn(scheduler)
|
|
||||||
todoLV = new TodoListView(lv)
|
|
||||||
todoComponent <- wire[TodoListComponent.Props].create
|
|
||||||
// TODO make this a "message pass" instead of mutating directly
|
|
||||||
_ <- Task(_scene.getChildren += lv).executeOn(scheduler)
|
|
||||||
_ <- toIO(
|
|
||||||
delObs
|
|
||||||
.doOnNext(_ => toTask(logger.debug("Pressed delete")))
|
|
||||||
.doOnNext(todo =>
|
|
||||||
toTask(
|
|
||||||
for {
|
|
||||||
_ <- logger.debug(s"Got todo $todo")
|
|
||||||
_ <- todoComponent.send(TodoListComponent.Delete(todo.id))
|
|
||||||
// _ <- Task.sequence(
|
|
||||||
// lst.map(todo =>
|
|
||||||
// todoComponent.send(TodoListComponent.Delete(todo.id))
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
} yield ()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.completedL
|
|
||||||
).startAndForget
|
|
||||||
_ <- toIO(
|
|
||||||
editObs
|
|
||||||
.doOnNext(_ => toTask(logger.debug("Pressed edit")))
|
|
||||||
.completedL
|
|
||||||
).startAndForget
|
|
||||||
} yield todoComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package nova.monadic_sfx.implicits
|
||||||
|
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.reactive.Observer
|
||||||
|
import monix.{eval => me}
|
||||||
|
import monix.execution.Scheduler
|
||||||
|
|
||||||
|
class ActionObservableExecuter[T](
|
||||||
|
delegate: Observable[T]
|
||||||
|
) {
|
||||||
|
def -->(sub: Observer[T])(implicit s: Scheduler) =
|
||||||
|
delegate
|
||||||
|
.doOnNext(el => me.Task(sub.onNext(el)))
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
}
|
||||||
|
class ActionObservableBuilder[A](
|
||||||
|
observableAction: Observable[A]
|
||||||
|
) {
|
||||||
|
def useLazy[T](v: => T) =
|
||||||
|
new ActionObservableExecuter[T](observableAction.mapEval(_ => me.Task(v)))
|
||||||
|
|
||||||
|
def use[T](cb: A => T) =
|
||||||
|
new ActionObservableExecuter[T](
|
||||||
|
observableAction.mapEval(ae => me.Task(cb(ae)))
|
||||||
|
)
|
||||||
|
|
||||||
|
def useIterable[T](cb: A => collection.immutable.Iterable[T]) =
|
||||||
|
new ActionObservableExecuter[T](
|
||||||
|
observableAction.flatMap(a =>
|
||||||
|
Observable.suspend(Observable.fromIterable(cb(a)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def map[B](cb: A => B) =
|
||||||
|
new ActionObservableBuilder(observableAction.mapEval(v => me.Task(cb(v))))
|
||||||
|
}
|
@ -39,4 +39,7 @@ class JFXButton(
|
|||||||
ripplerFill() = b
|
ripplerFill() = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def obsAction =
|
||||||
|
new ActionObservableBuilder(this.observableAction())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,12 @@ import scalafx.beans.value.ObservableValue
|
|||||||
import scalafx.collections.ObservableBuffer
|
import scalafx.collections.ObservableBuffer
|
||||||
import scalafx.scene.Scene
|
import scalafx.scene.Scene
|
||||||
import scalafx.scene.control.ButtonBase
|
import scalafx.scene.control.ButtonBase
|
||||||
|
import scalafx.beans.property.ReadOnlyProperty
|
||||||
|
import javafx.event.ActionEvent
|
||||||
|
import javafx.event.EventHandler
|
||||||
|
import javafx.scene.{control => jfxsc}
|
||||||
|
import scalafx.scene.control.MenuItem
|
||||||
|
import org.gerweck.scalafx.util._
|
||||||
|
|
||||||
object JavaFXMonixObservables {
|
object JavaFXMonixObservables {
|
||||||
|
|
||||||
@ -68,29 +74,35 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// implicit final class BindObs[T, J](private val prop: Property[T, J])
|
implicit final class BindObs[T, J](private val prop: Property[T, J])
|
||||||
// extends AnyVal {
|
extends AnyVal {
|
||||||
// def -->(op: Observer[T]) = {
|
def -->(op: Observer[T]) = {
|
||||||
// op.onNext(prop.value)
|
op.onNext(prop.value)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// def <--(obs: Observable[T])(implicit s: Scheduler) = {
|
def -->(op: Property[T, J]) = {
|
||||||
// obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
|
op() = prop.value
|
||||||
// }
|
}
|
||||||
|
|
||||||
// def observableChange[J1 >: J]()
|
def <--(obs: Observable[T])(implicit s: Scheduler) = {
|
||||||
// : Observable[(ObservableValue[T, J], J1, J1)] = {
|
obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
|
||||||
// import monix.execution.cancelables.SingleAssignCancelable
|
}
|
||||||
// Observable.create(OverflowStrategy.Unbounded) { sub =>
|
|
||||||
// val c = SingleAssignCancelable()
|
|
||||||
|
|
||||||
// val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
|
def asOption = prop.map(Option(_))
|
||||||
|
|
||||||
// c := Cancelable(() => canc.cancel())
|
def observableChange[J1 >: J]()
|
||||||
// c
|
: Observable[(ObservableValue[T, J], J1, J1)] = {
|
||||||
// }
|
import monix.execution.cancelables.SingleAssignCancelable
|
||||||
// }
|
Observable.create(OverflowStrategy.Unbounded) { sub =>
|
||||||
// }
|
val c = SingleAssignCancelable()
|
||||||
|
|
||||||
|
val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
|
||||||
|
|
||||||
|
c := Cancelable(() => canc.cancel())
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implicit final class BindObs2[A](private val prop: ObjectProperty[A])
|
implicit final class BindObs2[A](private val prop: ObjectProperty[A])
|
||||||
extends AnyVal {
|
extends AnyVal {
|
||||||
@ -107,6 +119,10 @@ object JavaFXMonixObservables {
|
|||||||
// op(prop.observableChange().map(_._3)).runToFuture
|
// op(prop.observableChange().map(_._3)).runToFuture
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
def -->(op: Property[A, A]) = {
|
||||||
|
prop.onChange((a, b, c) => if (c != null) op() = c)
|
||||||
|
}
|
||||||
|
|
||||||
def <--(obs: Observable[A])(implicit s: Scheduler) = {
|
def <--(obs: Observable[A])(implicit s: Scheduler) = {
|
||||||
obs.doOnNext(v => me.Task(prop() = v)).subscribe()
|
obs.doOnNext(v => me.Task(prop() = v)).subscribe()
|
||||||
}
|
}
|
||||||
@ -125,6 +141,34 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicit final class BindObs3[T, J](private val prop: ReadOnlyProperty[T, J])
|
||||||
|
extends AnyVal {
|
||||||
|
def -->(op: Observer[T]) = {
|
||||||
|
op.onNext(prop.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def -->(op: Property[T, J]) = {
|
||||||
|
op <== prop
|
||||||
|
}
|
||||||
|
|
||||||
|
// def <--(obs: Observable[T])(implicit s: Scheduler) = {
|
||||||
|
// obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
|
||||||
|
// }
|
||||||
|
|
||||||
|
def observableChange[J1 >: J]()
|
||||||
|
: Observable[(ObservableValue[T, J], J1, J1)] = {
|
||||||
|
import monix.execution.cancelables.SingleAssignCancelable
|
||||||
|
Observable.create(OverflowStrategy.Unbounded) { sub =>
|
||||||
|
val c = SingleAssignCancelable()
|
||||||
|
|
||||||
|
val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
|
||||||
|
|
||||||
|
c := Cancelable(() => canc.cancel())
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implicit final class ObjectPropertyObservableListExt[A](
|
implicit final class ObjectPropertyObservableListExt[A](
|
||||||
private val prop: ObjectProperty[ObservableList[A]]
|
private val prop: ObjectProperty[ObservableList[A]]
|
||||||
) extends AnyVal {
|
) extends AnyVal {
|
||||||
@ -166,6 +210,20 @@ object JavaFXMonixObservables {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicit final class ObjectPropertyActionEvent(
|
||||||
|
private val prop: ObjectProperty[EventHandler[ActionEvent]]
|
||||||
|
) extends AnyVal {
|
||||||
|
// def <--(obs: Observable[ActionEvent])(implicit s: Scheduler) = {
|
||||||
|
// obs.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v))).subscribe()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// def -->(sub: Observer[ActionEvent]) =
|
||||||
|
// prop().
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// def emit(prop: ObjectProperty[EventHandler[ActionEvent]]) =
|
||||||
|
|
||||||
implicit final class OnActionObservable(
|
implicit final class OnActionObservable(
|
||||||
private val button: ButtonBase
|
private val button: ButtonBase
|
||||||
) extends AnyVal {
|
) extends AnyVal {
|
||||||
@ -202,4 +260,41 @@ object JavaFXMonixObservables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicit final class MenuItemActionObservable(
|
||||||
|
private val et: MenuItem
|
||||||
|
) extends AnyVal {
|
||||||
|
// def -->[T](
|
||||||
|
// op: Observable[jfxe.ActionEvent] => me.Task[T]
|
||||||
|
// )(implicit s: Scheduler) = {
|
||||||
|
// op(button.observableAction()).runToFuture
|
||||||
|
// }
|
||||||
|
|
||||||
|
// def -->(
|
||||||
|
// sub: ConcurrentSubject[jfxe.ActionEvent, jfxe.ActionEvent]
|
||||||
|
// ) = {
|
||||||
|
// button.onAction = value => sub.onNext(value)
|
||||||
|
// }
|
||||||
|
|
||||||
|
def observableAction(): Observable[jfxe.ActionEvent] = {
|
||||||
|
import monix.execution.cancelables.SingleAssignCancelable
|
||||||
|
Observable.create(OverflowStrategy.Unbounded) { sub =>
|
||||||
|
val c = SingleAssignCancelable()
|
||||||
|
val l = new jfxe.EventHandler[jfxe.ActionEvent] {
|
||||||
|
override def handle(event: jfxe.ActionEvent): Unit = {
|
||||||
|
if (sub.onNext(event) == Ack.Stop) c.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
et.onAction = l
|
||||||
|
c := Cancelable(() =>
|
||||||
|
et.removeEventHandler(
|
||||||
|
jfxe.ActionEvent.ACTION,
|
||||||
|
l
|
||||||
|
)
|
||||||
|
)
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
9
src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
Normal file
9
src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package nova.monadic_sfx.implicits
|
||||||
|
|
||||||
|
import JavaFXMonixObservables._
|
||||||
|
import scalafx.scene.{control => sfxc}
|
||||||
|
|
||||||
|
class MenuItem extends sfxc.MenuItem {
|
||||||
|
def obsAction =
|
||||||
|
new ActionObservableBuilder(this.observableAction())
|
||||||
|
}
|
@ -1,556 +0,0 @@
|
|||||||
package nova.monadic_sfx.ui.components.todo
|
|
||||||
|
|
||||||
import scala.concurrent.Future
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
|
||||||
|
|
||||||
import cats.effect.Sync
|
|
||||||
import cats.effect.concurrent.Deferred
|
|
||||||
import io.odin.Logger
|
|
||||||
import monix.bio.Task
|
|
||||||
import monix.catnap.ConcurrentChannel
|
|
||||||
import monix.catnap.ConsumerF
|
|
||||||
import monix.execution.Ack
|
|
||||||
import monix.execution.Cancelable
|
|
||||||
import monix.execution.Scheduler
|
|
||||||
import monix.reactive.Observable
|
|
||||||
import monix.reactive.Observer
|
|
||||||
import monix.reactive.OverflowStrategy
|
|
||||||
import monix.reactive.observers.Subscriber
|
|
||||||
import monix.reactive.subjects.ConcurrentSubject
|
|
||||||
import nova.monadic_sfx.implicits.FontIcon
|
|
||||||
import nova.monadic_sfx.implicits.IconLiteral
|
|
||||||
import nova.monadic_sfx.implicits.JFXListView
|
|
||||||
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Add
|
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Delete
|
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Edit
|
|
||||||
import scalafx.Includes._
|
|
||||||
import scalafx.beans.property.StringProperty
|
|
||||||
import scalafx.collections.ObservableBuffer
|
|
||||||
import scalafx.scene.control.ContextMenu
|
|
||||||
import scalafx.scene.control.ListCell
|
|
||||||
import scalafx.scene.control.MenuItem
|
|
||||||
import scalafx.scene.control.SelectionMode
|
|
||||||
import scalafx.scene.layout.HBox
|
|
||||||
import scalafx.scene.text.Text
|
|
||||||
import nova.monadic_sfx.ui.components.todo.Store.MonixProSubject
|
|
||||||
import nova.monadic_sfx.util.IOUtils
|
|
||||||
import monix.tail.Iterant
|
|
||||||
|
|
||||||
case class Todo(id: Int, content: String)
|
|
||||||
|
|
||||||
class TodoListView(
|
|
||||||
val listView: JFXListView[Todo] = TodoListView.defaultListView,
|
|
||||||
val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty
|
|
||||||
) {
|
|
||||||
listView.items = lvObs
|
|
||||||
}
|
|
||||||
|
|
||||||
object TodoListView {
|
|
||||||
def defaultListView =
|
|
||||||
new JFXListView[Todo] {
|
|
||||||
// cellFactory = _ =>
|
|
||||||
// new ListCell[Todo] {
|
|
||||||
// // item.onChange((a, b, c) => ())
|
|
||||||
// overr
|
|
||||||
// }
|
|
||||||
contextMenu = new ContextMenu {
|
|
||||||
items ++= Seq(
|
|
||||||
new MenuItem {
|
|
||||||
text = "delete"
|
|
||||||
},
|
|
||||||
new MenuItem {
|
|
||||||
text = "edit"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// import scalafx.scene.control.MultipleSelectionModel
|
|
||||||
// .getOrElse(Todo(-1, "blah"))
|
|
||||||
implicit class Operations[A](val sink: Observer[A]) extends AnyVal {}
|
|
||||||
|
|
||||||
// def reducer(
|
|
||||||
// stateC: Coeval[ObservableBuffer[Todo]],
|
|
||||||
// action: TodoListComponent.Command
|
|
||||||
// ) =
|
|
||||||
// action match {
|
|
||||||
// case Add(todo) =>
|
|
||||||
// for {
|
|
||||||
// state <- stateC
|
|
||||||
// } yield state :+ todo
|
|
||||||
// // case Find(id, result) =>
|
|
||||||
// case Edit(id, content) => stateC
|
|
||||||
// case Delete(id) =>
|
|
||||||
// for {
|
|
||||||
// state <- stateC
|
|
||||||
// } yield state.filterNot(_.id == id)
|
|
||||||
// case _ => stateC
|
|
||||||
// }
|
|
||||||
|
|
||||||
def reducer(
|
|
||||||
state: Vector[Todo],
|
|
||||||
action: TodoListComponent.Command
|
|
||||||
) =
|
|
||||||
action match {
|
|
||||||
case Add(todo) => state :+ todo
|
|
||||||
// case Find(id, result) =>
|
|
||||||
case Edit(id, content) => state
|
|
||||||
case Delete(id) =>
|
|
||||||
state.filterNot(_.id == id)
|
|
||||||
case _ => state
|
|
||||||
}
|
|
||||||
|
|
||||||
def defaultListView2: Task[
|
|
||||||
(
|
|
||||||
JFXListView[Todo],
|
|
||||||
Observable[Todo],
|
|
||||||
Observable[Todo]
|
|
||||||
)
|
|
||||||
] =
|
|
||||||
Task.deferAction(implicit s =>
|
|
||||||
Store
|
|
||||||
.createL[TodoListComponent.Command, Vector[Todo]](
|
|
||||||
TodoListComponent.Delete(0),
|
|
||||||
Vector.empty[Todo],
|
|
||||||
(s: Vector[Todo], a: TodoListComponent.Command) =>
|
|
||||||
reducer(s, a) -> Observable.empty
|
|
||||||
)
|
|
||||||
.flatMap(store =>
|
|
||||||
Task {
|
|
||||||
val deleteSub = ConcurrentSubject.publish[Todo]
|
|
||||||
val editSub = ConcurrentSubject.publish[Todo]
|
|
||||||
|
|
||||||
// store.flatMap(st => Task(st.sink))
|
|
||||||
|
|
||||||
// val deleteSub2 =
|
|
||||||
// deleteSub.map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo)
|
|
||||||
// val addSub =
|
|
||||||
// ConcurrentSubject
|
|
||||||
// .publish[Todo]
|
|
||||||
// .map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo)
|
|
||||||
// val state = Observable(deleteSub2, addSub).merge.scan0(
|
|
||||||
// ObservableBuffer.empty[Todo]
|
|
||||||
// )((buf, fn) => fn(buf))
|
|
||||||
|
|
||||||
val todos =
|
|
||||||
store.map { case (_, items) => items }
|
|
||||||
|
|
||||||
val listView = new JFXListView[Todo] { lv =>
|
|
||||||
def selectedItems = lv.selectionModel().selectedItems.view
|
|
||||||
// items = todos
|
|
||||||
items <-- todos
|
|
||||||
// .map(ObservableBuffer.from(_))
|
|
||||||
cellFactory = _ =>
|
|
||||||
new ListCell[Todo] {
|
|
||||||
val _text = StringProperty("")
|
|
||||||
val _graphic = new HBox {
|
|
||||||
children = Seq(
|
|
||||||
new FontIcon {
|
|
||||||
iconSize = 10
|
|
||||||
iconLiteral = IconLiteral.Gmi10k
|
|
||||||
},
|
|
||||||
new Text {
|
|
||||||
text <== _text
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item.onChange((_, _, todo) => {
|
|
||||||
println("called")
|
|
||||||
if (todo != null) {
|
|
||||||
_text() = s"${todo.id} - ${todo.content}"
|
|
||||||
graphic = _graphic
|
|
||||||
} else {
|
|
||||||
_text() = ""
|
|
||||||
graphic = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
selectionModel().selectionMode = SelectionMode.Multiple
|
|
||||||
contextMenu = new ContextMenu {
|
|
||||||
items ++= Seq(
|
|
||||||
new MenuItem {
|
|
||||||
text = "Add"
|
|
||||||
onAction = _ =>
|
|
||||||
store.sink
|
|
||||||
.onNext(TodoListComponent.Add(Todo(1, "blah3")))
|
|
||||||
},
|
|
||||||
new MenuItem {
|
|
||||||
text = "Delete"
|
|
||||||
// onAction = _ =>
|
|
||||||
// for {
|
|
||||||
// items <- Option(lv.selectionModel().selectedItems)
|
|
||||||
// _ <- Some(items.foreach(item => deleteSub.onNext(item)))
|
|
||||||
// } yield ()
|
|
||||||
onAction = _ =>
|
|
||||||
selectedItems
|
|
||||||
.map(todo => TodoListComponent.Delete(todo.id))
|
|
||||||
.foreach(store.sink.onNext)
|
|
||||||
},
|
|
||||||
new MenuItem {
|
|
||||||
text = "Edit"
|
|
||||||
// onAction = _ =>
|
|
||||||
// Option(lv.selectionModel().selectedItems).foreach(items =>
|
|
||||||
// items.foreach(item => editSub.onNext(item))
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(listView, deleteSub, editSub)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private[todo] class TodoListComponentImpure(
|
|
||||||
todoListView: TodoListView
|
|
||||||
) {
|
|
||||||
def add(todo: Todo) = todoListView.lvObs += todo
|
|
||||||
def find(id: Int) = todoListView.lvObs.find(_.id == id)
|
|
||||||
def edit(id: Int, content: String) =
|
|
||||||
find(id)
|
|
||||||
.map(todo =>
|
|
||||||
todoListView.lvObs.replaceAll(
|
|
||||||
todo,
|
|
||||||
Todo(id, content)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.getOrElse(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
class TodoListOps private (
|
|
||||||
props: TodoListOps.Props
|
|
||||||
) {
|
|
||||||
import props._
|
|
||||||
// lazy val internal = new TodoListComponentImpure(todoListView)
|
|
||||||
|
|
||||||
// def add(todo: Todo) = Task(internal.add(todo))
|
|
||||||
def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler)
|
|
||||||
def find(id: Int) =
|
|
||||||
Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler)
|
|
||||||
def delete(id: Int) =
|
|
||||||
(for {
|
|
||||||
mbTodo <- find(id)
|
|
||||||
_ <- logger.debug(mbTodo.toString())
|
|
||||||
res <- Task(
|
|
||||||
mbTodo.map(todo => todoListView.lvObs.removeAll(todo))
|
|
||||||
)
|
|
||||||
_ <- logger.debug(todoListView.lvObs.toString())
|
|
||||||
} yield res.getOrElse(false)).executeOn(fxScheduler)
|
|
||||||
def edit(id: Int, content: String) =
|
|
||||||
(for {
|
|
||||||
mbTodo <- find(id)
|
|
||||||
res <- Task(
|
|
||||||
mbTodo.map(todo =>
|
|
||||||
todoListView.lvObs.replaceAll(
|
|
||||||
todo,
|
|
||||||
Todo(id, content)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} yield res.getOrElse(false)).executeOn(fxScheduler)
|
|
||||||
}
|
|
||||||
|
|
||||||
object TodoListOps {
|
|
||||||
class Props(
|
|
||||||
val todoListView: TodoListView,
|
|
||||||
val fxScheduler: Scheduler,
|
|
||||||
val logger: Logger[Task]
|
|
||||||
) {
|
|
||||||
def create = Task(new TodoListOps(this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object TodoListComponent {
|
|
||||||
sealed trait Complete
|
|
||||||
object Complete extends Complete
|
|
||||||
|
|
||||||
sealed trait Command
|
|
||||||
// sealed trait Tell extends Command
|
|
||||||
// sealed abstract class Ask extends Command
|
|
||||||
|
|
||||||
case class Add(todo: Todo) extends Command
|
|
||||||
case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command
|
|
||||||
case class Edit(id: Int, content: String) extends Command
|
|
||||||
case class Delete(id: Int) extends Command
|
|
||||||
// private case class FindInternal(id: Int, result: Deferred[Task, Todo])
|
|
||||||
// extends Ask
|
|
||||||
|
|
||||||
class Props(
|
|
||||||
val todoListView: TodoListView,
|
|
||||||
val fxScheduler: Scheduler,
|
|
||||||
val channel: ConcurrentChannel[
|
|
||||||
Task,
|
|
||||||
TodoListComponent.Complete,
|
|
||||||
TodoListComponent.Command
|
|
||||||
],
|
|
||||||
val logger: Logger[Task]
|
|
||||||
) {
|
|
||||||
|
|
||||||
def create =
|
|
||||||
for {
|
|
||||||
todoListOps <-
|
|
||||||
new TodoListOps.Props(todoListView, fxScheduler, logger).create
|
|
||||||
consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps))
|
|
||||||
_ <- consumer.startAndForget
|
|
||||||
} yield (new TodoListComponent(this))
|
|
||||||
|
|
||||||
private def todoConsumer(
|
|
||||||
consumer: ConsumerF[Task, Complete, Command],
|
|
||||||
ops: TodoListOps
|
|
||||||
): Task[Unit] =
|
|
||||||
consumer.pull
|
|
||||||
.flatMap {
|
|
||||||
case Left(complete) => logger.info("Received `Complete` event")
|
|
||||||
case Right(command) =>
|
|
||||||
logger.debug(s"Received command $command") >>
|
|
||||||
(command match {
|
|
||||||
// case t: Tell =>
|
|
||||||
// t match {
|
|
||||||
// case Add(todo) => ops.add(todo)
|
|
||||||
// case _ => Task.unit
|
|
||||||
// }
|
|
||||||
case Add(todo) => ops.add(todo)
|
|
||||||
// case Find(id) =>
|
|
||||||
// for {
|
|
||||||
// p <- Deferred[Task, Todo]
|
|
||||||
// _ <- channel.push(FindInternal(id, p))
|
|
||||||
// res <- p.get
|
|
||||||
// } yield (res)
|
|
||||||
case Find(id, result) =>
|
|
||||||
for {
|
|
||||||
mbTodo <- ops.find(id)
|
|
||||||
} yield result.complete(mbTodo)
|
|
||||||
// case _ => Task.unit
|
|
||||||
|
|
||||||
case Delete(id) => ops.delete(id)
|
|
||||||
|
|
||||||
case Edit(id, content) => ops.edit(id, content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.flatMap(_ => todoConsumer(consumer, ops))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TodoListComponent(props: TodoListComponent.Props) {
|
|
||||||
import props._
|
|
||||||
import TodoListComponent._
|
|
||||||
|
|
||||||
def send(command: Command) = channel.push(command)
|
|
||||||
|
|
||||||
def ask[T](
|
|
||||||
commandBuilder: Deferred[Task, T] => Command
|
|
||||||
)(implicit timeout: FiniteDuration) =
|
|
||||||
for {
|
|
||||||
p <- Deferred[Task, T]
|
|
||||||
_ <- channel.push(commandBuilder(p))
|
|
||||||
res <- p.get.timeout(timeout)
|
|
||||||
} yield res
|
|
||||||
|
|
||||||
def stop = channel.halt(Complete)
|
|
||||||
|
|
||||||
// import scala.concurrent.duration._
|
|
||||||
// val x = ask(FindInternal(0, _))(2.seconds)
|
|
||||||
|
|
||||||
}
|
|
||||||
// : F[ProSubject[A, (A, M)]]
|
|
||||||
|
|
||||||
// interface Middleware<S, A> {
|
|
||||||
// fun dispatch(store: Store<S, A>, next: (A) -> Unit, action: A)
|
|
||||||
// }
|
|
||||||
|
|
||||||
trait Middleware[A, M] {
|
|
||||||
def dispatch[T](
|
|
||||||
store: MonixProSubject[A, (A, M)],
|
|
||||||
cb: (A, M) => Task[T],
|
|
||||||
cb2: (A, M) => Observable[(A, M)],
|
|
||||||
cb3: Observable[(A, M)] => Observable[(A, M)]
|
|
||||||
) = {
|
|
||||||
// store.fil
|
|
||||||
store.mapEval {
|
|
||||||
case (a, m) => IOUtils.toTask(cb(a, m))
|
|
||||||
}
|
|
||||||
store.flatMap {
|
|
||||||
case (a, m) => cb2(a, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
cb3(store)
|
|
||||||
|
|
||||||
def cb3impl(obs: Observable[(A, M)]) =
|
|
||||||
obs.doOnNext {
|
|
||||||
case (a, m) => IOUtils.toTask(Task(println("hello")))
|
|
||||||
}
|
|
||||||
|
|
||||||
def cb3impl2(obs: Observable[(A, M)]) =
|
|
||||||
obs.filter {
|
|
||||||
case (a, m) => m == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
cb3impl2(cb3impl(store))
|
|
||||||
|
|
||||||
val s = Seq(cb3impl _)
|
|
||||||
|
|
||||||
val res = s.foldLeft(Observable.empty[(A, M)]) {
|
|
||||||
case (o1, o2) => o2(o1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val x = Iterant[Task].of(1, 2, 3)
|
|
||||||
|
|
||||||
// x match {
|
|
||||||
// case Next(item, rest) => ()
|
|
||||||
// case Halt(e) => ()
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Store {
|
|
||||||
type Reducer[A, M] = (M, A) => (M, Observable[A])
|
|
||||||
type MonixProSubject[-I, +O] = Observable[O] with Observer[I]
|
|
||||||
// class MonixProSubject2[-I, +O] extends Subject[I, O]
|
|
||||||
object MonixProSubject {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def createL[A, M](
|
|
||||||
initialAction: A,
|
|
||||||
initialState: M,
|
|
||||||
reducer: Reducer[A, M],
|
|
||||||
overflowStrategy: OverflowStrategy.Synchronous[A] =
|
|
||||||
OverflowStrategy.DropOld(50)
|
|
||||||
) =
|
|
||||||
Task.deferAction { implicit s =>
|
|
||||||
Task {
|
|
||||||
val subject = ConcurrentSubject.publish[A](overflowStrategy)
|
|
||||||
|
|
||||||
val fold: ((A, M), A) => (A, M) = {
|
|
||||||
case ((_, state), action) => {
|
|
||||||
val (newState, effects) = reducer(state, action)
|
|
||||||
|
|
||||||
effects.subscribe(subject.onNext _)
|
|
||||||
|
|
||||||
action -> newState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MonixProSubject.from(
|
|
||||||
subject,
|
|
||||||
subject
|
|
||||||
.scan[(A, M)](initialAction -> initialState)(fold)
|
|
||||||
.behavior(initialAction -> initialState)
|
|
||||||
.refCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def create[F[_], A, M](
|
|
||||||
initialAction: A,
|
|
||||||
initialState: M,
|
|
||||||
reducer: Reducer[A, M]
|
|
||||||
)(implicit s: Scheduler, F: Sync[F]): F[Observable[(A, M)]] =
|
|
||||||
F.delay {
|
|
||||||
val subject = ConcurrentSubject.publish[A]
|
|
||||||
|
|
||||||
val fold: ((A, M), A) => (A, M) = {
|
|
||||||
case ((_, state), action) => {
|
|
||||||
val (newState, effects) = reducer(state, action)
|
|
||||||
|
|
||||||
effects.subscribe(subject.onNext _)
|
|
||||||
|
|
||||||
action -> newState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subject
|
|
||||||
.scan[(A, M)](initialAction -> initialState)(fold)
|
|
||||||
.behavior(initialAction -> initialState)
|
|
||||||
.refCount
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// object TodoListComponent {
|
|
||||||
// sealed trait Complete
|
|
||||||
// object Complete extends Complete
|
|
||||||
|
|
||||||
// sealed trait Command
|
|
||||||
|
|
||||||
// class Props(
|
|
||||||
// val todoListView: TodoListView,
|
|
||||||
// val fxScheduler: Scheduler,
|
|
||||||
// val channel: ConcurrentChannel[
|
|
||||||
// Task,
|
|
||||||
// TodoListComponent.Complete,
|
|
||||||
// TodoListComponent.Command
|
|
||||||
// ]
|
|
||||||
// ) {
|
|
||||||
// def create = Task(new TodoListComponent(this))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class TodoListComponent(props: TodoListComponent.Props) {
|
|
||||||
// import props._
|
|
||||||
// import TodoListComponent._
|
|
||||||
// def init =
|
|
||||||
// for {
|
|
||||||
// todoListOps <- new TodoListOps.Props(todoListView, fxScheduler).create
|
|
||||||
// consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps))
|
|
||||||
// _ <- consumer.startAndForget
|
|
||||||
// } yield ()
|
|
||||||
|
|
||||||
// def send(command: Command) = channel.push(command)
|
|
||||||
|
|
||||||
// def todoConsumer(
|
|
||||||
// consumer: ConsumerF[Task, Complete, Command],
|
|
||||||
// ops: TodoListOps
|
|
||||||
// ) =
|
|
||||||
// consumer.pull.flatMap {
|
|
||||||
// case Left(value) => Task.unit
|
|
||||||
// case Right(value) => Task.unit
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// def askHandler(
|
|
||||||
// channel: ConcurrentChannel[
|
|
||||||
// Task,
|
|
||||||
// TodoListComponent.Complete,
|
|
||||||
// TodoListComponent.Command
|
|
||||||
// ],
|
|
||||||
// consumer: ConsumerF[Task, Complete, Command],
|
|
||||||
// ops: TodoListOps
|
|
||||||
// ) =
|
|
||||||
// consumer.pull.flatMap {
|
|
||||||
// case Left(complete) => Task.unit
|
|
||||||
// case Right(command) =>
|
|
||||||
// command match {
|
|
||||||
// case a: Ask =>
|
|
||||||
// a match {
|
|
||||||
// case Find(id) =>
|
|
||||||
// for {
|
|
||||||
// p <- Deferred[Task, Todo]
|
|
||||||
// _ <- channel.push(FindInternal(id, p))
|
|
||||||
// res <- p.get
|
|
||||||
// } yield (res)
|
|
||||||
// case FindInternal(id, result) =>
|
|
||||||
// for {
|
|
||||||
// mb <- ops.find(id)
|
|
||||||
// } yield result.complete(mb.get)
|
|
||||||
// case _ => Task.unit
|
|
||||||
// }
|
|
||||||
// case _ => Task.unit
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -0,0 +1,378 @@
|
|||||||
|
package nova.monadic_sfx.ui.components.todo
|
||||||
|
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
|
||||||
|
import cats.effect.concurrent.Deferred
|
||||||
|
import io.odin.Logger
|
||||||
|
import monix.bio.Task
|
||||||
|
import monix.catnap.ConcurrentChannel
|
||||||
|
import monix.catnap.ConsumerF
|
||||||
|
import monix.execution.Scheduler
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.reactive.Observer
|
||||||
|
import nova.monadic_sfx.implicits.FontIcon
|
||||||
|
import nova.monadic_sfx.implicits.IconLiteral
|
||||||
|
import nova.monadic_sfx.implicits.JFXListView
|
||||||
|
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
||||||
|
import scalafx.Includes._
|
||||||
|
import scalafx.beans.property.StringProperty
|
||||||
|
import scalafx.collections.ObservableBuffer
|
||||||
|
import scalafx.scene.control.ContextMenu
|
||||||
|
import scalafx.scene.control.ListCell
|
||||||
|
import scalafx.scene.control.MenuItem
|
||||||
|
import scalafx.scene.control.SelectionMode
|
||||||
|
import scalafx.scene.layout.HBox
|
||||||
|
import scalafx.scene.text.Text
|
||||||
|
import nova.monadic_sfx.util.reactive._
|
||||||
|
|
||||||
|
class TodoListViewOld(
|
||||||
|
val listView: JFXListView[Todo] = TodoListViewOld.defaultListView,
|
||||||
|
val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty
|
||||||
|
) {
|
||||||
|
listView.items = lvObs
|
||||||
|
}
|
||||||
|
|
||||||
|
object TodoListViewOld {
|
||||||
|
def defaultListView =
|
||||||
|
new JFXListView[Todo] {
|
||||||
|
contextMenu = new ContextMenu {
|
||||||
|
items ++= Seq(
|
||||||
|
new MenuItem {
|
||||||
|
text = "delete"
|
||||||
|
},
|
||||||
|
new MenuItem {
|
||||||
|
text = "edit"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit class Operations[A](val sink: Observer[A]) extends AnyVal {}
|
||||||
|
|
||||||
|
def defaultListView2(
|
||||||
|
store: MonixProSubject[
|
||||||
|
TodoListComponentOld.Command,
|
||||||
|
(TodoListComponentOld.Command, Vector[Todo])
|
||||||
|
]
|
||||||
|
): Task[JFXListView[Todo]] =
|
||||||
|
Task.deferAction(implicit s =>
|
||||||
|
Task {
|
||||||
|
val todos =
|
||||||
|
store.map { case (_, items) => items }
|
||||||
|
|
||||||
|
val listView = new JFXListView[Todo] { lv =>
|
||||||
|
def selectedItems = lv.selectionModel().selectedItems.view
|
||||||
|
// items = todos
|
||||||
|
items <-- todos
|
||||||
|
// .map(ObservableBuffer.from(_))
|
||||||
|
cellFactory = _ =>
|
||||||
|
new ListCell[Todo] {
|
||||||
|
val _text = StringProperty("")
|
||||||
|
val _graphic = new HBox {
|
||||||
|
children = Seq(
|
||||||
|
new FontIcon {
|
||||||
|
iconSize = 10
|
||||||
|
iconLiteral = IconLiteral.Gmi10k
|
||||||
|
},
|
||||||
|
new Text {
|
||||||
|
text <== _text
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item.onChange((_, _, todo) => {
|
||||||
|
println("called")
|
||||||
|
if (todo != null) {
|
||||||
|
_text() = s"${todo.id} - ${todo.content}"
|
||||||
|
graphic = _graphic
|
||||||
|
} else {
|
||||||
|
_text() = ""
|
||||||
|
graphic = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
selectionModel().selectionMode = SelectionMode.Multiple
|
||||||
|
contextMenu = new ContextMenu {
|
||||||
|
items ++= Seq(
|
||||||
|
new MenuItem {
|
||||||
|
text = "Add"
|
||||||
|
onAction = _ =>
|
||||||
|
store.sink
|
||||||
|
.onNext(TodoListComponentOld.Add(Todo(1, "blah3")))
|
||||||
|
},
|
||||||
|
new MenuItem {
|
||||||
|
text = "Delete"
|
||||||
|
// onAction = _ =>
|
||||||
|
// for {
|
||||||
|
// items <- Option(lv.selectionModel().selectedItems)
|
||||||
|
// _ <- Some(items.foreach(item => deleteSub.onNext(item)))
|
||||||
|
// } yield ()
|
||||||
|
onAction = _ =>
|
||||||
|
selectedItems
|
||||||
|
.map(todo => TodoListComponentOld.Delete(todo.id))
|
||||||
|
.foreach(store.sink.onNext)
|
||||||
|
},
|
||||||
|
new MenuItem {
|
||||||
|
text = "Edit"
|
||||||
|
// onAction = _ =>
|
||||||
|
// Option(lv.selectionModel().selectedItems).foreach(items =>
|
||||||
|
// items.foreach(item => editSub.onNext(item))
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listView
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private[todo] class TodoListComponentImpure(
|
||||||
|
todoListView: TodoListViewOld
|
||||||
|
) {
|
||||||
|
def add(todo: Todo) = todoListView.lvObs += todo
|
||||||
|
def find(id: Int) = todoListView.lvObs.find(_.id == id)
|
||||||
|
def edit(id: Int, content: String) =
|
||||||
|
find(id)
|
||||||
|
.map(todo =>
|
||||||
|
todoListView.lvObs.replaceAll(
|
||||||
|
todo,
|
||||||
|
Todo(id, content)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.getOrElse(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoListOps private (
|
||||||
|
props: TodoListOps.Props
|
||||||
|
) {
|
||||||
|
import props._
|
||||||
|
// lazy val internal = new TodoListComponentImpure(todoListView)
|
||||||
|
|
||||||
|
// def add(todo: Todo) = Task(internal.add(todo))
|
||||||
|
def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler)
|
||||||
|
def find(id: Int) =
|
||||||
|
Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler)
|
||||||
|
def delete(id: Int) =
|
||||||
|
(for {
|
||||||
|
mbTodo <- find(id)
|
||||||
|
_ <- logger.debug(mbTodo.toString())
|
||||||
|
res <- Task(
|
||||||
|
mbTodo.map(todo => todoListView.lvObs.removeAll(todo))
|
||||||
|
)
|
||||||
|
_ <- logger.debug(todoListView.lvObs.toString())
|
||||||
|
} yield res.getOrElse(false)).executeOn(fxScheduler)
|
||||||
|
def edit(id: Int, content: String) =
|
||||||
|
(for {
|
||||||
|
mbTodo <- find(id)
|
||||||
|
res <- Task(
|
||||||
|
mbTodo.map(todo =>
|
||||||
|
todoListView.lvObs.replaceAll(
|
||||||
|
todo,
|
||||||
|
Todo(id, content)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} yield res.getOrElse(false)).executeOn(fxScheduler)
|
||||||
|
}
|
||||||
|
|
||||||
|
object TodoListOps {
|
||||||
|
class Props(
|
||||||
|
val todoListView: TodoListViewOld,
|
||||||
|
val fxScheduler: Scheduler,
|
||||||
|
val logger: Logger[Task]
|
||||||
|
) {
|
||||||
|
def create = Task(new TodoListOps(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object TodoListComponentOld {
|
||||||
|
sealed trait Complete
|
||||||
|
object Complete extends Complete
|
||||||
|
|
||||||
|
sealed trait Command
|
||||||
|
// sealed trait Tell extends Command
|
||||||
|
// sealed abstract class Ask extends Command
|
||||||
|
|
||||||
|
case class Add(todo: Todo) extends Command
|
||||||
|
case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command
|
||||||
|
case class Edit(id: Int, content: String) extends Command
|
||||||
|
case class Delete(id: Int) extends Command
|
||||||
|
// private case class FindInternal(id: Int, result: Deferred[Task, Todo])
|
||||||
|
// extends Ask
|
||||||
|
|
||||||
|
def reducer(
|
||||||
|
state: Vector[Todo],
|
||||||
|
action: TodoListComponentOld.Command
|
||||||
|
) =
|
||||||
|
action match {
|
||||||
|
case Add(todo) => state :+ todo
|
||||||
|
// case Find(id, result) =>
|
||||||
|
case Edit(id, content) => state
|
||||||
|
case Delete(id) =>
|
||||||
|
state.filterNot(_.id == id)
|
||||||
|
case _ => state
|
||||||
|
}
|
||||||
|
|
||||||
|
val store =
|
||||||
|
Store
|
||||||
|
.createL[TodoListComponentOld.Command, Vector[Todo]](
|
||||||
|
TodoListComponentOld.Delete(0),
|
||||||
|
Vector.empty[Todo],
|
||||||
|
(s: Vector[Todo], a: TodoListComponentOld.Command) =>
|
||||||
|
reducer(s, a) -> Observable.empty
|
||||||
|
)
|
||||||
|
|
||||||
|
class Props(
|
||||||
|
val todoListView: TodoListViewOld,
|
||||||
|
val fxScheduler: Scheduler,
|
||||||
|
val channel: ConcurrentChannel[
|
||||||
|
Task,
|
||||||
|
TodoListComponentOld.Complete,
|
||||||
|
TodoListComponentOld.Command
|
||||||
|
],
|
||||||
|
val logger: Logger[Task]
|
||||||
|
) {
|
||||||
|
|
||||||
|
def create =
|
||||||
|
for {
|
||||||
|
todoListOps <-
|
||||||
|
new TodoListOps.Props(todoListView, fxScheduler, logger).create
|
||||||
|
consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps))
|
||||||
|
_ <- consumer.startAndForget
|
||||||
|
} yield (new TodoListComponentOld(this))
|
||||||
|
|
||||||
|
private def todoConsumer(
|
||||||
|
consumer: ConsumerF[Task, Complete, Command],
|
||||||
|
ops: TodoListOps
|
||||||
|
): Task[Unit] =
|
||||||
|
consumer.pull
|
||||||
|
.flatMap {
|
||||||
|
case Left(complete) => logger.info("Received `Complete` event")
|
||||||
|
case Right(command) =>
|
||||||
|
logger.debug(s"Received command $command") >>
|
||||||
|
(command match {
|
||||||
|
// case t: Tell =>
|
||||||
|
// t match {
|
||||||
|
// case Add(todo) => ops.add(todo)
|
||||||
|
// case _ => Task.unit
|
||||||
|
// }
|
||||||
|
case Add(todo) => ops.add(todo)
|
||||||
|
// case Find(id) =>
|
||||||
|
// for {
|
||||||
|
// p <- Deferred[Task, Todo]
|
||||||
|
// _ <- channel.push(FindInternal(id, p))
|
||||||
|
// res <- p.get
|
||||||
|
// } yield (res)
|
||||||
|
case Find(id, result) =>
|
||||||
|
for {
|
||||||
|
mbTodo <- ops.find(id)
|
||||||
|
} yield result.complete(mbTodo)
|
||||||
|
// case _ => Task.unit
|
||||||
|
|
||||||
|
case Delete(id) => ops.delete(id)
|
||||||
|
|
||||||
|
case Edit(id, content) => ops.edit(id, content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.flatMap(_ => todoConsumer(consumer, ops))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoListComponentOld(props: TodoListComponentOld.Props) {
|
||||||
|
import props._
|
||||||
|
import TodoListComponentOld._
|
||||||
|
|
||||||
|
def send(command: Command) = channel.push(command)
|
||||||
|
|
||||||
|
def ask[T](
|
||||||
|
commandBuilder: Deferred[Task, T] => Command
|
||||||
|
)(implicit timeout: FiniteDuration) =
|
||||||
|
for {
|
||||||
|
p <- Deferred[Task, T]
|
||||||
|
_ <- channel.push(commandBuilder(p))
|
||||||
|
res <- p.get.timeout(timeout)
|
||||||
|
} yield res
|
||||||
|
|
||||||
|
def stop = channel.halt(Complete)
|
||||||
|
|
||||||
|
// import scala.concurrent.duration._
|
||||||
|
// val x = ask(FindInternal(0, _))(2.seconds)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// object TodoListComponent {
|
||||||
|
// sealed trait Complete
|
||||||
|
// object Complete extends Complete
|
||||||
|
|
||||||
|
// sealed trait Command
|
||||||
|
|
||||||
|
// class Props(
|
||||||
|
// val todoListView: TodoListView,
|
||||||
|
// val fxScheduler: Scheduler,
|
||||||
|
// val channel: ConcurrentChannel[
|
||||||
|
// Task,
|
||||||
|
// TodoListComponent.Complete,
|
||||||
|
// TodoListComponent.Command
|
||||||
|
// ]
|
||||||
|
// ) {
|
||||||
|
// def create = Task(new TodoListComponent(this))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class TodoListComponent(props: TodoListComponent.Props) {
|
||||||
|
// import props._
|
||||||
|
// import TodoListComponent._
|
||||||
|
// def init =
|
||||||
|
// for {
|
||||||
|
// todoListOps <- new TodoListOps.Props(todoListView, fxScheduler).create
|
||||||
|
// consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps))
|
||||||
|
// _ <- consumer.startAndForget
|
||||||
|
// } yield ()
|
||||||
|
|
||||||
|
// def send(command: Command) = channel.push(command)
|
||||||
|
|
||||||
|
// def todoConsumer(
|
||||||
|
// consumer: ConsumerF[Task, Complete, Command],
|
||||||
|
// ops: TodoListOps
|
||||||
|
// ) =
|
||||||
|
// consumer.pull.flatMap {
|
||||||
|
// case Left(value) => Task.unit
|
||||||
|
// case Right(value) => Task.unit
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// def askHandler(
|
||||||
|
// channel: ConcurrentChannel[
|
||||||
|
// Task,
|
||||||
|
// TodoListComponent.Complete,
|
||||||
|
// TodoListComponent.Command
|
||||||
|
// ],
|
||||||
|
// consumer: ConsumerF[Task, Complete, Command],
|
||||||
|
// ops: TodoListOps
|
||||||
|
// ) =
|
||||||
|
// consumer.pull.flatMap {
|
||||||
|
// case Left(complete) => Task.unit
|
||||||
|
// case Right(command) =>
|
||||||
|
// command match {
|
||||||
|
// case a: Ask =>
|
||||||
|
// a match {
|
||||||
|
// case Find(id) =>
|
||||||
|
// for {
|
||||||
|
// p <- Deferred[Task, Todo]
|
||||||
|
// _ <- channel.push(FindInternal(id, p))
|
||||||
|
// res <- p.get
|
||||||
|
// } yield (res)
|
||||||
|
// case FindInternal(id, result) =>
|
||||||
|
// for {
|
||||||
|
// mb <- ops.find(id)
|
||||||
|
// } yield result.complete(mb.get)
|
||||||
|
// case _ => Task.unit
|
||||||
|
// }
|
||||||
|
// case _ => Task.unit
|
||||||
|
// }
|
||||||
|
// }
|
@ -0,0 +1,67 @@
|
|||||||
|
package nova.monadic_sfx.ui.components.todo
|
||||||
|
|
||||||
|
import nova.monadic_sfx.util.reactive.Store
|
||||||
|
import nova.monadic_sfx.util.reactive.Reducer
|
||||||
|
import nova.monadic_sfx.util.reactive.Middlewares.actionLoggerMiddleware
|
||||||
|
import io.odin.Logger
|
||||||
|
import monix.bio.Task
|
||||||
|
import io.odin._
|
||||||
|
import io.odin.syntax._
|
||||||
|
import io.circe.generic.JsonCodec
|
||||||
|
import com.softwaremill.quicklens._
|
||||||
|
|
||||||
|
case class Todo(id: Int, content: String)
|
||||||
|
|
||||||
|
object TodoListStore {
|
||||||
|
|
||||||
|
@JsonCodec
|
||||||
|
sealed trait Command
|
||||||
|
case object Init extends Command
|
||||||
|
case class Add(content: String) extends Command
|
||||||
|
case class Edit(id: Int, content: String) extends Command
|
||||||
|
case class Delete(id: Int) extends Command
|
||||||
|
|
||||||
|
case class State(todos: Vector[Todo], counter: Int)
|
||||||
|
|
||||||
|
def reducer(
|
||||||
|
state: State,
|
||||||
|
action: Command
|
||||||
|
) =
|
||||||
|
action match {
|
||||||
|
case Init => state
|
||||||
|
case Add(content) =>
|
||||||
|
state.copy(
|
||||||
|
todos = state.todos :+ Todo(state.counter, content),
|
||||||
|
counter = state.counter + 1
|
||||||
|
)
|
||||||
|
case Edit(id, content) =>
|
||||||
|
state
|
||||||
|
.modify(_.todos.eachWhere(_.id == id))
|
||||||
|
.using(_.copy(content = content))
|
||||||
|
case Delete(id) =>
|
||||||
|
state.copy(state.todos.filterNot(_.id == id))
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateTodo(id: Int, content: String, todos: Vector[Todo]) =
|
||||||
|
todos.view.zipWithIndex
|
||||||
|
.find {
|
||||||
|
case (todo, index) => todo.id == id
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
case (todo, index) => todo.copy(content = content) -> index
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
case (todo, index) => todos.updated(index, todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
val middlewareLogger = consoleLogger[Task]().withAsync()
|
||||||
|
|
||||||
|
def apply(logger: Logger[Task]) =
|
||||||
|
Store
|
||||||
|
.createL[Command, State](
|
||||||
|
Init,
|
||||||
|
State(Vector.empty[Todo], 0),
|
||||||
|
Reducer(reducer _),
|
||||||
|
Seq(actionLoggerMiddleware(logger))
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package nova.monadic_sfx.ui.components.todo
|
||||||
|
|
||||||
|
import monix.bio.Task
|
||||||
|
import nova.monadic_sfx.implicits.FontIcon
|
||||||
|
import nova.monadic_sfx.implicits.IconLiteral
|
||||||
|
import nova.monadic_sfx.implicits.JFXListView
|
||||||
|
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
||||||
|
import scalafx.Includes._
|
||||||
|
import scalafx.beans.property.StringProperty
|
||||||
|
import scalafx.scene.control.ContextMenu
|
||||||
|
import scalafx.scene.control.ListCell
|
||||||
|
import scalafx.scene.control.SelectionMode
|
||||||
|
import scalafx.scene.layout.HBox
|
||||||
|
import scalafx.scene.text.Text
|
||||||
|
import nova.monadic_sfx.util.reactive._
|
||||||
|
import org.gerweck.scalafx.util._
|
||||||
|
import scalafx.beans.property.ObjectProperty
|
||||||
|
import nova.monadic_sfx.implicits.MenuItem
|
||||||
|
import monix.execution.cancelables.CompositeCancelable
|
||||||
|
|
||||||
|
object TodoListView {
|
||||||
|
def apply(
|
||||||
|
store: MonixProSubject[
|
||||||
|
TodoListStore.Command,
|
||||||
|
(TodoListStore.Command, TodoListStore.State)
|
||||||
|
]
|
||||||
|
): Task[JFXListView[Todo]] =
|
||||||
|
Task.deferAction(implicit s =>
|
||||||
|
Task {
|
||||||
|
val cc = CompositeCancelable()
|
||||||
|
val todos =
|
||||||
|
store.map { case (_, state) => state.todos }
|
||||||
|
|
||||||
|
new JFXListView[Todo] {
|
||||||
|
def selectedItems = selectionModel().selectedItems.view
|
||||||
|
|
||||||
|
cc += items <-- todos
|
||||||
|
|
||||||
|
val emptyCell = ObjectProperty(new HBox)
|
||||||
|
cellFactory = _ =>
|
||||||
|
new ListCell[Todo] {
|
||||||
|
val _text = StringProperty("")
|
||||||
|
val _graphic = ObjectProperty(
|
||||||
|
new HBox {
|
||||||
|
children = Seq(
|
||||||
|
new FontIcon {
|
||||||
|
iconSize = 10
|
||||||
|
iconLiteral = IconLiteral.Gmi10k
|
||||||
|
},
|
||||||
|
new Text {
|
||||||
|
text <== _text
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
item.asOption.map(
|
||||||
|
_.fold("")(todo => s"${todo.id} - ${todo.content}")
|
||||||
|
) --> _text
|
||||||
|
|
||||||
|
graphic <== item.asOption.flatMap(
|
||||||
|
_.fold(emptyCell)(_ => _graphic)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionModel().selectionMode = SelectionMode.Multiple
|
||||||
|
|
||||||
|
contextMenu = new ContextMenu {
|
||||||
|
items ++= Seq(
|
||||||
|
new MenuItem {
|
||||||
|
text = "Add"
|
||||||
|
obsAction.useLazy(TodoListStore.Add("blah3")) --> store
|
||||||
|
},
|
||||||
|
new MenuItem {
|
||||||
|
text = "Delete"
|
||||||
|
obsAction.useIterable(_ =>
|
||||||
|
selectedItems
|
||||||
|
.map(todo => TodoListStore.Delete(todo.id))
|
||||||
|
.toList
|
||||||
|
) --> store
|
||||||
|
},
|
||||||
|
new MenuItem {
|
||||||
|
text = "Edit"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -12,13 +12,13 @@ import nova.monadic_sfx.implicits.JFXButton
|
|||||||
import nova.monadic_sfx.implicits.JFXListView
|
import nova.monadic_sfx.implicits.JFXListView
|
||||||
import nova.monadic_sfx.implicits.JFXTextArea
|
import nova.monadic_sfx.implicits.JFXTextArea
|
||||||
import nova.monadic_sfx.implicits.JFXTextField
|
import nova.monadic_sfx.implicits.JFXTextField
|
||||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent
|
|
||||||
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
|
||||||
import scalafx.scene.paint.Color
|
import scalafx.scene.paint.Color
|
||||||
|
import nova.monadic_sfx.ui.components.todo.TodoListComponentOld
|
||||||
|
|
||||||
class TodoController(todoListComponent: TodoListComponent) {
|
class TodoController(todoListComponent: TodoListComponentOld) {
|
||||||
import AnimFX._
|
import AnimFX._
|
||||||
def root =
|
def root =
|
||||||
new HBox {
|
new HBox {
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package nova.monadic_sfx.util.reactive
|
||||||
|
|
||||||
|
import io.odin.Logger
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.bio.Task
|
||||||
|
import nova.monadic_sfx.util.IOUtils._
|
||||||
|
|
||||||
|
// object Middleware {
|
||||||
|
// def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob
|
||||||
|
// }
|
||||||
|
object Middlewares {
|
||||||
|
def actionStateLoggerMiddleware[A, M](
|
||||||
|
logger: Logger[Task]
|
||||||
|
): Middleware[A, M] =
|
||||||
|
(obs: Observable[(A, M)]) =>
|
||||||
|
obs.doOnNext {
|
||||||
|
case (a, m) => toTask(logger.debug(s"Received action $a with state $m"))
|
||||||
|
}
|
||||||
|
|
||||||
|
def actionLoggerMiddleware[A, M](
|
||||||
|
logger: Logger[Task]
|
||||||
|
): Middleware[A, M] =
|
||||||
|
(obs: Observable[(A, M)]) =>
|
||||||
|
obs.doOnNext {
|
||||||
|
case (a, _) => toTask(logger.debug(s"Received action $a "))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package nova.monadic_sfx.util.reactive
|
||||||
|
|
||||||
|
import monix.execution.Cancelable
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.reactive.Observer
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import monix.reactive.observers.Subscriber
|
||||||
|
import monix.execution.Ack
|
||||||
|
|
||||||
|
object MonixProSubject {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
39
src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala
Normal file
39
src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package nova.monadic_sfx.util.reactive
|
||||||
|
|
||||||
|
import monix.reactive.ObservableLike
|
||||||
|
import cats.implicits._
|
||||||
|
import monix.reactive.Observable
|
||||||
|
|
||||||
|
object Reducer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Reducer which yields a new State, as-well as an Observable of Effects
|
||||||
|
* Effects are Actions which will be executed after the Action that caused them to occur.
|
||||||
|
* This is accomplished by subscribing to the Effects Observable within the stores scan loop.
|
||||||
|
*
|
||||||
|
* CAUTION: There is currently a bug which causes the Effect-States to emit,
|
||||||
|
* before the State of the action that caused the effects is emitted.
|
||||||
|
* However, this only effects immediate emissions of the Effects Observable, delayed emissions should be fine.
|
||||||
|
* @param f The Reducing Function returning the (Model, Effects) tuple.
|
||||||
|
*/
|
||||||
|
def withEffects[F[_]: ObservableLike, A, M](
|
||||||
|
f: (M, A) => (M, F[A])
|
||||||
|
): Reducer[A, M] = (s: M, a: A) => f(s, a).map(ObservableLike[F].apply)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reducer which just transforms the state, without additional effects.
|
||||||
|
*/
|
||||||
|
def apply[A, M](f: (M, A) => M): Reducer[A, M] =
|
||||||
|
(s: M, a: A) => f(s, a) -> Observable.empty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Reducer with an optional effect.
|
||||||
|
*/
|
||||||
|
def withOptionalEffects[F[_]: ObservableLike, A, M](
|
||||||
|
f: (M, A) => (M, Option[F[A]])
|
||||||
|
): Reducer[A, M] =
|
||||||
|
(s: M, a: A) =>
|
||||||
|
f(s, a).map(
|
||||||
|
_.fold[Observable[A]](Observable.empty)(ObservableLike[F].apply)
|
||||||
|
)
|
||||||
|
}
|
73
src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
Normal file
73
src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package nova.monadic_sfx.util.reactive
|
||||||
|
|
||||||
|
import monix.reactive.OverflowStrategy
|
||||||
|
import monix.reactive.subjects.ConcurrentSubject
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.bio.Task
|
||||||
|
import cats.effect.Sync
|
||||||
|
import monix.execution.Scheduler
|
||||||
|
|
||||||
|
object Store {
|
||||||
|
def createL[A, M](
|
||||||
|
initialAction: A,
|
||||||
|
initialState: M,
|
||||||
|
reducer: Reducer[A, M],
|
||||||
|
middlewares: Seq[Middleware[A, M]] = Seq.empty,
|
||||||
|
overflowStrategy: OverflowStrategy.Synchronous[A] =
|
||||||
|
OverflowStrategy.DropOld(50)
|
||||||
|
) =
|
||||||
|
Task.deferAction { implicit s =>
|
||||||
|
Task {
|
||||||
|
val subject = ConcurrentSubject.publish[A](overflowStrategy)
|
||||||
|
|
||||||
|
val fold: ((A, M), A) => (A, M) = {
|
||||||
|
case ((_, state), action) => {
|
||||||
|
val (newState, effects) = reducer(state, action)
|
||||||
|
|
||||||
|
effects.subscribe(subject.onNext _)
|
||||||
|
|
||||||
|
action -> newState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val obs = subject
|
||||||
|
.scan[(A, M)](initialAction -> initialState)(fold)
|
||||||
|
.behavior(initialAction -> initialState)
|
||||||
|
.refCount
|
||||||
|
|
||||||
|
val res = middlewares.view.reverse.foldLeft(obs) {
|
||||||
|
case (obs, middleware) => middleware(obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
MonixProSubject.from(
|
||||||
|
subject,
|
||||||
|
res
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create[F[_], A, M](
|
||||||
|
initialAction: A,
|
||||||
|
initialState: M,
|
||||||
|
reducer: Reducer[A, M]
|
||||||
|
)(implicit s: Scheduler, F: Sync[F]): F[Observable[(A, M)]] =
|
||||||
|
F.delay {
|
||||||
|
val subject = ConcurrentSubject.publish[A]
|
||||||
|
|
||||||
|
val fold: ((A, M), A) => (A, M) = {
|
||||||
|
case ((_, state), action) => {
|
||||||
|
val (newState, effects) = reducer(state, action)
|
||||||
|
|
||||||
|
effects.subscribe(subject.onNext _)
|
||||||
|
|
||||||
|
action -> newState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subject
|
||||||
|
.scan[(A, M)](initialAction -> initialState)(fold)
|
||||||
|
.behavior(initialAction -> initialState)
|
||||||
|
.refCount
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
src/main/scala/nova/monadic_sfx/util/reactive/package.scala
Normal file
17
src/main/scala/nova/monadic_sfx/util/reactive/package.scala
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package nova.monadic_sfx.util
|
||||||
|
|
||||||
|
import monix.reactive.Observable
|
||||||
|
import monix.reactive.Observer
|
||||||
|
|
||||||
|
package object reactive {
|
||||||
|
type MonixProSubject[-I, +O] = Observable[O] with Observer[I]
|
||||||
|
type Middleware[A, M] = Observable[(A, M)] => Observable[(A, M)]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Function that applies an Action onto the Stores current state.
|
||||||
|
* @param reducer The reducing function
|
||||||
|
* @tparam A The Action Type
|
||||||
|
* @tparam M The Model Type
|
||||||
|
*/
|
||||||
|
type Reducer[A, M] = (M, A) => (M, Observable[A])
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user