Browse Source
Many changes
Many changes
Updated store code - wrapped fold function in Task Added JSON logging functionality to store middleware Initial attempt at creating filter combinator for fx observables Made ListStore code use Effects Made a router using the store pattern Misc updates to fx monix implicitsmaster
Rohan Sircar
3 years ago
15 changed files with 518 additions and 115 deletions
-
6build.sbt
-
9src/main/scala/nova/monadic_sfx/Main.scala
-
46src/main/scala/nova/monadic_sfx/MainApp.scala
-
36src/main/scala/nova/monadic_sfx/MainModule.scala
-
2src/main/scala/nova/monadic_sfx/Types.scala
-
103src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
-
3src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
-
16src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala
-
107src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala
-
73src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala
-
20src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala
-
63src/main/scala/nova/monadic_sfx/util/Misc.scala
-
72src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala
-
52src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
-
1src/main/scala/nova/monadic_sfx/util/reactive/package.scala
@ -1,7 +1,41 @@ |
|||
package nova.monadic_sfx |
|||
|
|||
import scala.concurrent.duration._ |
|||
|
|||
import _root_.monix.bio.Task |
|||
import cats.implicits._ |
|||
import io.odin._ |
|||
import io.odin.config._ |
|||
import io.odin.syntax._ |
|||
import nova.monadic_sfx.actors.ActorModule |
|||
import nova.monadic_sfx.http.HttpModule |
|||
import nova.monadic_sfx.ui.UiModule |
|||
import nova.monadic_sfx.util.reactive.Middlewares |
|||
|
|||
trait MainModule extends ActorModule with UiModule with HttpModule { |
|||
def routerLogger(defaultLogger: Logger[Task], storeLogger: Logger[Task]) = |
|||
enclosureRouting[Task]( |
|||
"nova.monadic_sfx.util.reactive.Middlewares" -> storeLogger, |
|||
"nova.monadic_sfx.util.reactive.Store" -> storeLogger |
|||
) |
|||
.withFallback(defaultLogger) |
|||
.withAsync() |
|||
|
|||
trait MainModule extends ActorModule with UiModule with HttpModule |
|||
def makeLogger = |
|||
for { |
|||
defaultLogger <- consoleLogger[Task]() |
|||
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( |
|||
"application.log" |
|||
).withAsync() |
|||
middlewareLogger <- |
|||
consoleLogger[ |
|||
Task |
|||
](formatter = Middlewares.format) |
|||
.withMinimalLevel(Level.Trace) |
|||
.withAsync() |+| fileLogger[Task]( |
|||
"stores.log", |
|||
formatter = Middlewares.format |
|||
).withAsync() |
|||
routerLogger <- routerLogger(defaultLogger, middlewareLogger) |
|||
} yield (routerLogger) |
|||
} |
@ -0,0 +1,107 @@ |
|||
package nova.monadic_sfx.ui.components.router |
|||
|
|||
import enumeratum._ |
|||
import io.circe.Encoder |
|||
import io.circe.generic.JsonCodec |
|||
import io.odin.Logger |
|||
import monix.bio.Task |
|||
import nova.monadic_sfx.util.IOUtils |
|||
import nova.monadic_sfx.util.reactive.Reducer |
|||
import nova.monadic_sfx.util.reactive.Store |
|||
import scalafx.scene.Parent |
|||
import scalafx.scene.control.Label |
|||
|
|||
object FXRouter { |
|||
|
|||
final case class State[P](page: P) |
|||
|
|||
@JsonCodec |
|||
sealed abstract class Action[T] |
|||
// final case object Init extends Action |
|||
final case class Replace[T](p: T) extends Action[T] |
|||
|
|||
type FXStore[P] = Store[Action[P], State[P]] |
|||
|
|||
// def resolver2 = resolver.lift.andThen(_.getOrElse(notFound)) |
|||
|
|||
// def resolver: PartialFunction[P <: Enum[P]][P, Parent] = { |
|||
// case Home => new TextField |
|||
// } |
|||
|
|||
} |
|||
|
|||
class FXRouter[P <: EnumEntry]( |
|||
)(implicit E: Encoder[P]) { |
|||
import FXRouter._ |
|||
|
|||
def store(initialPage: P, logger: Logger[Task]) = |
|||
Task.deferAction(implicit s => |
|||
Store.createL[Action[P], State[P]]( |
|||
Replace(initialPage), |
|||
State(initialPage), |
|||
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _) |
|||
// Seq(actionLoggerMiddleware(logger, "RouterStore")) |
|||
) |
|||
) |
|||
|
|||
def reducer( |
|||
state: State[P], |
|||
action: Action[P] |
|||
): (State[P], Option[Task[Action[P]]]) = |
|||
action match { |
|||
// case Init => (state, None) |
|||
case Replace(p) => |
|||
(state.copy(page = p), None) |
|||
} |
|||
|
|||
def render( |
|||
resolver: P => Task[Parent] |
|||
)(implicit store: FXStore[P]) = |
|||
store.mapEval { case (_, FXRouter.State(p)) => IOUtils.toTask(resolver(p)) } |
|||
|
|||
def link( |
|||
page: P, |
|||
store: FXStore[P] |
|||
) = { |
|||
store.onNext(Replace(page)) |
|||
} |
|||
} |
|||
|
|||
object BrainNotWorking { |
|||
|
|||
@JsonCodec |
|||
sealed trait Page extends EnumEntry |
|||
object Page extends Enum[Page] { |
|||
val values = findValues |
|||
final case object Home extends Page |
|||
final case class UserHome(id: Int) extends Page |
|||
} |
|||
|
|||
def resolver: PartialFunction[Page, Task[Parent]] = { |
|||
case Page.Home => |
|||
Task(new Label { |
|||
text = "HomePage" |
|||
}) |
|||
case Page.UserHome(id0) => |
|||
Task(new Label { |
|||
text = s"User Home, Id = $id0" |
|||
}) |
|||
} |
|||
|
|||
val router = new FXRouter[Page] |
|||
} |
|||
|
|||
// case class State() |
|||
// object RouterStore { |
|||
|
|||
// sealed trait Action |
|||
// case object Init extends Action |
|||
|
|||
// def reducer(state: State, action: Action) = |
|||
// action match { |
|||
// case Init => state |
|||
|
|||
// } |
|||
// def apply() = |
|||
// Store.createL[Action, State](Init, State(), Reducer(reducer _), Seq.empty) |
|||
// } |
@ -0,0 +1,63 @@ |
|||
package nova.monadic_sfx.util |
|||
|
|||
import scalafx.beans.property.ObjectProperty |
|||
import scalafx.beans.property.ReadOnlyObjectProperty |
|||
import scalafx.beans.value.ObservableValue |
|||
|
|||
object Misc { |
|||
|
|||
implicit final class MyRichObservable[A, C](val self: ObservableValue[A, C]) |
|||
extends AnyVal { |
|||
def filter(f: A => Boolean): ReadOnlyObjectProperty[A] = |
|||
Method.filter(self)(f) |
|||
def filterNull: ReadOnlyObjectProperty[A] = Method.filterNull(self) |
|||
} |
|||
|
|||
} |
|||
object Method { |
|||
type Observable[A] = ObservableValue[A, _] |
|||
|
|||
def filter[B]( |
|||
a: Observable[B] |
|||
)(f: B => Boolean): ReadOnlyObjectProperty[B] = { |
|||
|
|||
val prop = ObjectProperty[B](a.value) |
|||
|
|||
def changeHandler() = |
|||
prop.synchronized { |
|||
if (f(a.value)) { |
|||
prop.value = a.value |
|||
} |
|||
} |
|||
|
|||
a onChange changeHandler() |
|||
prop |
|||
} |
|||
|
|||
/** |
|||
* Simply creates a new observable that mirrors the source observable but |
|||
* doesn't emit null values. JavaFX likes to work with null values in scene |
|||
* nodes/properties (shrugs) and observables by default emit null values |
|||
* that can cause crashes. ScalaFX does not offer any *fixes* for this |
|||
* |
|||
* @param a |
|||
* @return |
|||
*/ |
|||
def filterNull[B]( |
|||
a: Observable[B] |
|||
): ReadOnlyObjectProperty[B] = { |
|||
|
|||
val prop = ObjectProperty[B](a.value) |
|||
|
|||
def changeHandler() = |
|||
prop.synchronized { |
|||
if (a.value != null) { |
|||
prop.value = a.value |
|||
} |
|||
} |
|||
|
|||
a onChange changeHandler() |
|||
prop |
|||
} |
|||
|
|||
} |
@ -1,27 +1,75 @@ |
|||
package nova.monadic_sfx.util.reactive |
|||
|
|||
import java.time.LocalDateTime |
|||
|
|||
import io.circe.Encoder |
|||
import io.circe.Printer |
|||
import io.circe.generic.JsonCodec |
|||
import io.circe.syntax._ |
|||
import io.odin.Logger |
|||
import io.odin.LoggerMessage |
|||
import io.odin.formatter.options.PositionFormat |
|||
import io.odin.formatter.options.ThrowableFormat |
|||
import io.odin.meta.Render |
|||
import monix.bio.Task |
|||
import monix.reactive.Observable |
|||
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 |
|||
// } |
|||
|
|||
@JsonCodec |
|||
final case class StoreInfo[A]( |
|||
name: String, |
|||
action: A, |
|||
time: LocalDateTime = LocalDateTime.now() |
|||
) |
|||
|
|||
object StoreInfo { |
|||
val printer = Printer.noSpaces |
|||
implicit def render[T: Encoder]: Render[StoreInfo[T]] = |
|||
new Render[StoreInfo[T]] { |
|||
override def render(m: StoreInfo[T]): String = printer.print(m.asJson) |
|||
} |
|||
} |
|||
|
|||
object Middlewares { |
|||
|
|||
// val encoder: Encoder[LoggerMessage] = |
|||
// Encoder.forProduct1("message")(m => m.message.value) |
|||
|
|||
val format = create(ThrowableFormat.Default, PositionFormat.Full) |
|||
|
|||
def create( |
|||
throwableFormat: ThrowableFormat, |
|||
positionFormat: PositionFormat |
|||
): io.odin.formatter.Formatter = { |
|||
// val encoder: Encoder[LoggerMessage] = |
|||
// Encoder.forProduct1("message")(m => m.message.value) |
|||
(msg: LoggerMessage) => msg.message.value |
|||
} |
|||
|
|||
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")) |
|||
): Task[Middleware[A, M]] = |
|||
Task.deferAction(implicit s => |
|||
Task((obs: Observable[(A, M)]) => |
|||
obs.doOnNextF { |
|||
case (a, m) => |
|||
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 ")) |
|||
def actionLoggerMiddleware[A: Encoder, M]( |
|||
logger: Logger[Task], |
|||
name: String |
|||
): Task[Middleware[A, M]] = |
|||
Task.deferAction(implicit s => |
|||
Task((obs: Observable[(A, M)]) => |
|||
obs.doOnNextF { |
|||
case (a, _) => |
|||
logger.debug(StoreInfo(name, a)) |
|||
} |
|||
) |
|||
) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue