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" %% "async-http-client-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.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
|
||||
"com.softwaremill.macwire" %% "util" % "2.3.7",
|
||||
|
@ -23,10 +23,10 @@ object Main extends MainModule with BIOApp {
|
||||
"application.log"
|
||||
).withAsync()
|
||||
schedulers = new Schedulers()
|
||||
backend <- Resource.make(
|
||||
toIO(HttpClientMonixBackend()(schedulers.async))
|
||||
)(c => toIO(c.close()))
|
||||
actorSystem <- actorSystemResource(logger)
|
||||
// backend <- Resource.make(
|
||||
// toIO(HttpClientMonixBackend()(schedulers.async))
|
||||
// )(c => toIO(c.close()))
|
||||
// actorSystem <- actorSystemResource(logger)
|
||||
_ <- Resource.liftF(wire[MainApp].program)
|
||||
} yield ()
|
||||
|
||||
|
@ -3,15 +3,11 @@ package nova.monadic_sfx
|
||||
import com.softwaremill.macwire._
|
||||
import io.odin.Logger
|
||||
import monix.bio.Task
|
||||
import monix.catnap.ConcurrentChannel
|
||||
import nova.monadic_sfx.executors.Schedulers
|
||||
import nova.monadic_sfx.implicits.JFXButton
|
||||
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
|
||||
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.util.IOUtils._
|
||||
import org.gerweck.scalafx.util._
|
||||
import scalafx.Includes._
|
||||
import scalafx.application.JFXApp.PrimaryStage
|
||||
@ -25,6 +21,7 @@ import scalafx.scene.control.TableView
|
||||
import scalafx.scene.layout.HBox
|
||||
import scalafx.scene.paint.Color
|
||||
import scalafx.scene.shape.Rectangle
|
||||
import nova.monadic_sfx.ui.components.todo.TodoListStore
|
||||
|
||||
class MainApp(
|
||||
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
|
||||
@ -38,7 +35,7 @@ class MainApp(
|
||||
|
||||
lazy val addTodoObs = addTodoButton.observableAction()
|
||||
|
||||
lazy val todoListView = TodoListView.defaultListView
|
||||
// lazy val todoListView = TodoListView.defaultListView
|
||||
|
||||
lazy val _scene = new Scene {
|
||||
root = new HBox {
|
||||
@ -77,59 +74,66 @@ class MainApp(
|
||||
// _ <- Task(fxApp.stage = stage)
|
||||
// .executeOn(schedulers.fx)
|
||||
// .delayExecution(2000.millis)
|
||||
todoComponent <- createTodoComponent
|
||||
_ <- toIO(
|
||||
addTodoObs
|
||||
.mapEval(_ =>
|
||||
toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
|
||||
)
|
||||
.completedL
|
||||
.executeOn(schedulers.fx)
|
||||
.startAndForget
|
||||
)
|
||||
// todoComponent <- createTodoComponent
|
||||
// _ <- toIO(
|
||||
// addTodoObs
|
||||
// .mapEval(_ =>
|
||||
// toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
|
||||
// )
|
||||
// .completedL
|
||||
// .executeOn(schedulers.fx)
|
||||
// .startAndForget
|
||||
// )
|
||||
_ <- createTodoComponent.executeOn(schedulers.fx)
|
||||
_ <- logger.info(
|
||||
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds"
|
||||
)
|
||||
_ <- fxAppFib.join
|
||||
} 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 {
|
||||
channel <-
|
||||
ConcurrentChannel
|
||||
.of[Task, TodoListComponent.Complete, TodoListComponent.Command]
|
||||
scheduler = schedulers.fx
|
||||
(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
|
||||
}
|
||||
store <- TodoListStore(logger)
|
||||
lv <- TodoListView(store)
|
||||
_ <- Task(_scene.getChildren += lv)
|
||||
} yield ()
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
def obsAction =
|
||||
new ActionObservableBuilder(this.observableAction())
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,12 @@ import scalafx.beans.value.ObservableValue
|
||||
import scalafx.collections.ObservableBuffer
|
||||
import scalafx.scene.Scene
|
||||
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 {
|
||||
|
||||
@ -68,29 +74,35 @@ object JavaFXMonixObservables {
|
||||
}
|
||||
}
|
||||
|
||||
// implicit final class BindObs[T, J](private val prop: Property[T, J])
|
||||
// extends AnyVal {
|
||||
// def -->(op: Observer[T]) = {
|
||||
// op.onNext(prop.value)
|
||||
// }
|
||||
implicit final class BindObs[T, J](private val prop: Property[T, J])
|
||||
extends AnyVal {
|
||||
def -->(op: Observer[T]) = {
|
||||
op.onNext(prop.value)
|
||||
}
|
||||
|
||||
// def <--(obs: Observable[T])(implicit s: Scheduler) = {
|
||||
// obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
|
||||
// }
|
||||
def -->(op: Property[T, J]) = {
|
||||
op() = prop.value
|
||||
}
|
||||
|
||||
// def observableChange[J1 >: J]()
|
||||
// : Observable[(ObservableValue[T, J], J1, J1)] = {
|
||||
// import monix.execution.cancelables.SingleAssignCancelable
|
||||
// Observable.create(OverflowStrategy.Unbounded) { sub =>
|
||||
// val c = SingleAssignCancelable()
|
||||
def <--(obs: Observable[T])(implicit s: Scheduler) = {
|
||||
obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
|
||||
}
|
||||
|
||||
// val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
|
||||
def asOption = prop.map(Option(_))
|
||||
|
||||
// c := Cancelable(() => canc.cancel())
|
||||
// c
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
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 BindObs2[A](private val prop: ObjectProperty[A])
|
||||
extends AnyVal {
|
||||
@ -107,6 +119,10 @@ object JavaFXMonixObservables {
|
||||
// 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) = {
|
||||
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](
|
||||
private val prop: ObjectProperty[ObservableList[A]]
|
||||
) 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(
|
||||
private val button: ButtonBase
|
||||
) 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.JFXTextArea
|
||||
import nova.monadic_sfx.implicits.JFXTextField
|
||||
import nova.monadic_sfx.ui.components.todo.TodoListComponent
|
||||
import scalafx.collections.ObservableBuffer
|
||||
import scalafx.scene.control.Label
|
||||
import scalafx.scene.layout.HBox
|
||||
import scalafx.scene.paint.Color
|
||||
import nova.monadic_sfx.ui.components.todo.TodoListComponentOld
|
||||
|
||||
class TodoController(todoListComponent: TodoListComponent) {
|
||||
class TodoController(todoListComponent: TodoListComponentOld) {
|
||||
import AnimFX._
|
||||
def root =
|
||||
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