You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
556 lines
16 KiB
556 lines
16 KiB
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
|
|
// }
|
|
// }
|