Compare commits

...

11 Commits

Author SHA1 Message Date
LQ63
d9f7275854 Added link to click for player pov, started rendering with images 2025-10-20 15:17:17 +02:00
dad604186e Update submodule configuration for knockoutwhist to track webapplication branch 2025-10-17 10:48:47 +02:00
8e415390e7 Add HTML rendering for sessions and card images; refactor output handling in SimpleSession 2025-10-17 10:26:30 +02:00
fc751af1ef Add HTML rendering for sessions and card images; refactor output handling in SimpleSession 2025-10-16 08:12:26 +02:00
LQ63
9aa447f2f6 Added link to click for player pov, started rendering with images 2025-10-13 22:39:21 +02:00
2b89f3d161 Add WebUICards object for rendering cards and hands; update render methods in SimpleSession 2025-10-13 15:25:31 +02:00
c77eeff123 Refactor match handling and session management; add player session functionality and update event handling in WebUI 2025-10-13 15:16:41 +02:00
7cbb6e6ab7 Refactor Match class and rename WebUI to WebUIMain; update HomeController for new UI structure 2025-10-12 22:56:25 +02:00
LQ63
6c57abb1db Added WebUI observer, added route to see latestOutput. For some reason the WebUI Observer doesn't execute it's methods for writing stuff into latestOutput. This has something to do with how the threads work 2025-10-12 15:21:20 +02:00
LQ63
854189967f Added WebUI observer, added route to see latestOutput. For some reason the WebUI Observer doesn't execute it's methods for writing stuff into latestOutput. This has something to do with how the threads work 2025-10-12 15:18:55 +02:00
LQ63
9098b7c4e3 Added WebUI observer, added route to see latestOutput. For some reason the WebUI Observer doesn't execute it's methods for writing stuff into latestOutput. This has something to do with how the threads work 2025-10-12 15:05:40 +02:00
80 changed files with 540 additions and 30 deletions

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "knockoutwhist"]
path = knockoutwhist
branch = webapplication
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git

1
knockoutwhist Submodule

Submodule knockoutwhist added at 48cd4d3956

View File

@@ -1,9 +1,13 @@
package components
import de.knockoutwhist.components.DefaultConfiguration
import controllers.Gamesession
import de.knockoutwhist.ui.UI
import de.knockoutwhist.utils.events.EventListener
class WebApplicationConfiguration extends DefaultConfiguration {
override def uis: Set[UI] = super.uis + Gamesession
override def listener: Set[EventListener] = super.listener + Gamesession
}

View File

@@ -0,0 +1,57 @@
package controllers
import controllers.sessions.{AdvancedSession, PlayerSession, SimpleSession}
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
import de.knockoutwhist.events.*
import de.knockoutwhist.events.player.{PlayCardEvent, PlayerEvent, ReceivedHandEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
import de.knockoutwhist.player.AbstractPlayer
import de.knockoutwhist.rounds.Match
import de.knockoutwhist.ui.UI
import de.knockoutwhist.utils.CustomThread
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import java.util.UUID
import scala.collection.mutable
class Gamesession(id: UUID) extends CustomThread with EventListener with UI {
setName("Gamesession")
private val sessions: mutable.Map[UUID, PlayerSession] = mutable.Map()
var init = false
var logic: Option[GameLogic] = None
override def instance: CustomThread = this
override def listen(event: SimpleEvent): Unit = {
runLater {
event match {
case event: PlayCardEvent =>
PodGameManager.transmit(event.player.id, event)
case event: ReceivedHandEvent =>
PodGameManager.transmit(event.player.id, event)
case event: RequestTieChoiceEvent =>
PodGameManager.transmit(event.player.id, event)
case event: RequestTrumpSuitEvent =>
PodGameManager.transmit(event.player.id, event)
case _ =>
PodGameManager.transmitAll(event)
}
}
}
override def initial(gameLogic: GameLogic): Boolean = {
if (init) {
return false
}
this.logic = Some(gameLogic)
init = true
start()
true
}
def addSession(session: PlayerSession): Unit = {
sessions.put(session.id, session)
}
}

View File

@@ -1,9 +1,19 @@
package controllers
import javax.inject._
import play.api._
import play.api.mvc._
import controllers.sessions.SimpleSession
import controllers.sessions.AdvancedSession
import com.google.inject.{Guice, Injector}
import de.knockoutwhist.KnockOutWhist
import de.knockoutwhist.components.Configuration
import de.knockoutwhist.rounds.Match
import di.KnockOutWebConfigurationModule
import play.api.*
import play.api.mvc.*
import play.twirl.api.Html
import java.util.UUID
import javax.inject.*
/**
* This controller creates an `Action` to handle HTTP requests to the
@@ -13,6 +23,7 @@ import de.knockoutwhist.KnockOutWhist
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
private var initial = false
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
/**
* Create an Action to render an HTML page.
@@ -24,17 +35,32 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e
def index(): Action[AnyContent] = {
if (!initial) {
initial = true
KnockOutWhist.main(new Array[String](_length = 0))
}
Action { implicit request: Request[AnyContent] => {
Ok(views.html.main.apply("KnockoutWhist")(views.html.))
KnockOutWhist.entry(injector.getInstance(classOf[Configuration]))
}
Action { implicit request =>
Redirect("/sessions")
}
}
def ingame(): Action[AnyContent] = {
Action { implicit request: Request[AnyContent] => {
Ok(views.html.tui.apply())
def sessions(): Action[AnyContent] = {
Action { implicit request =>
Ok(views.html.sessions.apply(PodGameManager.listSessions().map(f => f.toString)))
}
}
def ingame(id: String): Action[AnyContent] = {
val uuid: UUID = UUID.fromString(id)
if (PodGameManager.identify(uuid).isEmpty) {
Action { implicit request =>
NotFound(views.html.tui.apply(List(Html(s"<p>Session with id $id not found!</p>"))))
}
} else {
val gamesession = PodGameManager.identify(uuid).get
val player = session.asInstanceOf[AdvancedSession].player
val logic =
Action { implicit request =>
Ok(views.html.matchy.apply(player, ))
}
}
}

View File

@@ -0,0 +1,41 @@
package controllers
import controllers.sessions.PlayerSession
import de.knockoutwhist.utils.events.SimpleEvent
import java.util.UUID
import scala.collection.mutable
object PodGameManager {
private val gamesession: mutable.Map[UUID, Gamesession] = mutable.Map()
def addGame(session: PlayerSession, gamesession: Gamesession): Unit = {
gamesession.put(session.id, gamesession)
}
def createGame(player: String): Unit = {
val game = Gamesession(UUID.randomUUID())
}
def clearSessions(): Unit = {
gamesession.clear()
}
def identify(id: UUID): Option[PlayerSession] = {
gamesession.get(id)
}
def transmit(id: UUID, event: SimpleEvent): Unit = {
identify(id).foreach(_.updatePlayer(event))
}
def transmitAll(event: SimpleEvent): Unit = {
gamesession.foreach(session => session._2.updatePlayer(event))
}
def listSessions(): List[UUID] = {
sessions.keys.toList
}
}

View File

@@ -1,12 +0,0 @@
package controllers
import de.knockoutwhist.ui.UI
object WebUI extends UI {
override def initial: Boolean = true
}

View File

@@ -0,0 +1,13 @@
package controllers.sessions
import de.knockoutwhist.rounds.Match
import de.knockoutwhist.rounds.Round
import de.knockoutwhist.utils.events.SimpleEvent
import de.knockoutwhist.player.AbstractPlayer
import java.util.UUID
case class AdvancedSession(player: AbstractPlayer) extends PlayerSession {
override def id(): UUID = {
player.id
}
}

View File

@@ -0,0 +1,12 @@
package controllers.sessions
import de.knockoutwhist.utils.events.SimpleEvent
import java.util.UUID
trait PlayerSession {
def id: UUID
def updatePlayer(event: SimpleEvent): Unit
}

View File

@@ -0,0 +1,260 @@
package controllers.sessions
import de.knockoutwhist.cards.{Card, CardValue, Hand}
import de.knockoutwhist.events.ERROR_STATUS.*
import de.knockoutwhist.events.GLOBAL_STATUS.*
import de.knockoutwhist.events.PLAYER_STATUS.*
import de.knockoutwhist.events.ROUND_STATUS.*
import de.knockoutwhist.events.{ShowErrorStatus, ShowGlobalStatus, ShowPlayerStatus, ShowRoundStatus}
import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent}
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
import de.knockoutwhist.player.AbstractPlayer
import de.knockoutwhist.utils.events.SimpleEvent
import play.twirl.api.Html
import scalafx.scene.image.Image
import util.WebUIUtils
import java.util.UUID
case class SimpleSession(id: UUID, private var output: List[Html]) extends PlayerSession {
def get(): List[Html] = {
output
}
override def updatePlayer(event: SimpleEvent): Unit = {
event match {
case event: RenderHandEvent =>
renderHand(event)
case event: ShowTieCardsEvent =>
showtiecardseventmethod(event)
case event: ShowGlobalStatus =>
showglobalstatusmethod(event)
case event: ShowPlayerStatus =>
showplayerstatusmethod(event)
case event: ShowRoundStatus =>
showroundstatusmethod(event)
case event: ShowErrorStatus =>
showerrstatmet(event)
case event: ShowCurrentTrickEvent =>
showcurtrevmet(event)
}
}
private def clear(): Unit = {
output = List()
}
private def renderHand(event: RenderHandEvent): Unit = {
output = output :++ WebUICards.renderHandEvent(event.hand)
output = output :+ Html("<br>")
}
private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = {
var l = List[Html]()
for ((player, card) <- event.card) {
l = l :+ Html(s"<p>${player.name}:</p>")
l = l :+ WebUIUtils.cardtoImage(card)
l = l :+ Html("<br>")
}
output = output :++ l
output = output :+ Html("<br>")
Some(true)
}
private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = {
event.status match {
case SHOW_TIE =>
println("It's a tie! Let's cut to determine the winner.")
Some(true)
case SHOW_TIE_WINNER =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} wins the cut!")
Some(true)
}
case SHOW_TIE_TIE =>
println("It's a tie again! Let's cut again.")
Some(true)
case SHOW_START_MATCH =>
clear()
println("Starting a new match...")
output = output :+ Html("<br><br>")
Some(true)
case SHOW_TYPE_PLAYERS =>
println("Please enter the names of the players, separated by a comma.")
Some(true)
case SHOW_FINISHED_MATCH =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
clear()
println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}")
Some(true)
}
}
}
private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = {
val player = event.player
event.status match {
case SHOW_PLAY_CARD =>
println("Which card do you want to play?")
Some(true)
case SHOW_DOG_PLAY_CARD =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Boolean]) {
None
} else {
println("You are using your dog life. Do you want to play your final card now?")
if (event.objects.head.asInstanceOf[Boolean]) {
println("You have to play your final card this round!")
println("Please enter y to play your final card.")
Some(true)
} else {
println("Please enter y/n to play your final card.")
Some(true)
}
}
case SHOW_TIE_NUMBERS =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Int]) {
None
} else {
println(s"${player.name} enter a number between 1 and ${event.objects.head.asInstanceOf[Int]}.")
Some(true)
}
case SHOW_TRUMPSUIT_OPTIONS =>
println("Which suit do you want to pick as the next trump suit?")
println("1: Hearts")
println("2: Diamonds")
println("3: Clubs")
println("4: Spades")
println()
Some(true)
case SHOW_NOT_PLAYED =>
println(s"Player ${event.player} decided to not play his card")
Some(true)
case SHOW_WON_PLAYER_TRICK =>
println(s"${event.player.name} won the trick.")
output = output :+ Html("<br><br>")
Some(true)
}
}
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
event.status match {
case SHOW_TURN =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
println(s"It's ${event.objects.head.asInstanceOf[AbstractPlayer].name} turn.")
Some(true)
}
case SHOW_START_ROUND =>
clear()
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
output = output :+ Html("<br><br>")
Some(true)
case WON_ROUND =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} won the round.")
Some(true)
}
case PLAYERS_OUT =>
println("The following players are out of the game:")
event.currentRound.playersout.foreach(p => {
println(p.name)
})
Some(true)
}
}
private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = {
event.status match {
case INVALID_NUMBER =>
println("Please enter a valid number.")
Some(true)
case NOT_A_NUMBER =>
println("Please enter a number.")
Some(true)
case INVALID_INPUT =>
println("Please enter a valid input")
Some(true)
case INVALID_NUMBER_OF_PLAYERS =>
println("Please enter at least two names.")
Some(true)
case IDENTICAL_NAMES =>
println("Please enter unique names.")
Some(true)
case INVALID_NAME_FORMAT =>
println("Please enter valid names. Those can not be empty, shorter than 2 or longer then 10 characters.")
Some(true)
case WRONG_CARD =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Card]) {
None
} else {
println(f"You have to play a card of suit: ${event.objects.head.asInstanceOf[Card].suit}\n")
Some(true)
}
}
}
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
clear()
val sb = new StringBuilder()
sb.append("Current Trick:\n")
sb.append("Trump-Suit: " + event.round.trumpSuit + "\n")
if (event.trick.firstCard.isDefined) {
sb.append(s"Suit to play: ${event.trick.firstCard.get.suit}\n")
}
for ((card, player) <- event.trick.cards) {
sb.append(s"${player.name} played ${card.toString}\n")
}
println(sb.toString())
Some(true)
}
private def println(s: String): Unit = {
var html = List[Html]()
for (line <- s.split("\n")) {
html = html :+ Html(line)
html = html :+ Html("<br>")
}
output = output :++ html
}
private def println(): Unit = {
output = output :+ Html("<br>")
}
object WebUICards {
def renderCardAsString(card: Card): Vector[String] = {
val lines = "│ │"
if (card.cardValue == CardValue.Ten) {
return Vector(
s"┌─────────┐",
s"${card.cardValue.cardType()}",
lines,
s"${card.suit.cardType()}",
lines,
s"${card.cardValue.cardType()}",
s"└─────────┘"
)
}
Vector(
s"┌─────────┐",
s"${card.cardValue.cardType()}",
lines,
s"${card.suit.cardType()}",
lines,
s"${card.cardValue.cardType()}",
s"└─────────┘"
)
}
def renderHandEvent(hand: Hand): List[Html] = {
hand.cards.map(WebUIUtils.cardtoImage)
}
}
}

View File

@@ -0,0 +1,13 @@
package di
import com.google.inject.AbstractModule
import components.WebApplicationConfiguration
import de.knockoutwhist.components.Configuration
class KnockOutWebConfigurationModule extends AbstractModule {
override def configure(): Unit = {
bind(classOf[Configuration]).to(classOf[WebApplicationConfiguration])
}
}

View File

@@ -0,0 +1,34 @@
package util
import de.knockoutwhist.cards.Card
import de.knockoutwhist.cards.CardValue.{Ace, Eight, Five, Four, Jack, King, Nine, Queen, Seven, Six, Ten, Three, Two}
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
import play.twirl.api.Html
import scalafx.scene.image.Image
object WebUIUtils {
def cardtoImage(card: Card): Html = {
val s = card.suit match {
case Spades => "S"
case Hearts => "H"
case Clubs => "C"
case Diamonds => "D"
}
val cv = card.cardValue match {
case Ace => "A"
case King => "K"
case Queen => "Q"
case Jack => "J"
case Ten => "T"
case Nine => "9"
case Eight => "8"
case Seven => "7"
case Six => "6"
case Five => "5"
case Four => "4"
case Three => "3"
case Two => "2"
}
views.html.output.card.apply(f"images/cards/$cv$s.png")(card.toString)
}
}

View File

@@ -1,5 +1,3 @@
@(output: String)
@main("Welcome to Play") {
<h1>Welcome to Play!</h1>
}

View File

@@ -0,0 +1,25 @@
@*
* This template is called from the `index` template. This template
* handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page.
*@
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
@* Here's where we render the page title `String`. *@
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
</head>
<body>
@* And here's where we render the `Html` object containing
* the page content. *@
@content
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
@(toRender: List[Any])
@main("Match") {
<div id="match">
@for(line <- toRender) {
@line
}
</div>
}

View File

@@ -0,0 +1,2 @@
@(src: String)(alt: String)
<img src="@routes.Assets.versioned(src)" alt="@alt"/>

View File

@@ -0,0 +1,3 @@
@(text: String)
<p>@text</p>

View File

@@ -0,0 +1,10 @@
@(toRender: List[String])
@main("Sessions") {
<div id="sessions">
@for(line <- toRender) {
<a href="@routes.HomeController.ingame(line)">@line</a><br>
}
</div>
}

View File

@@ -1 +1,10 @@
@()
@(toRender: List[Html])
@main("Tui") {
<div id="tui">
@for(line <- toRender) {
@line
}
</div>
}

View File

@@ -4,8 +4,9 @@
# ~~~~
# An example controller showing a sample home page
GET / controllers.HomeController.index()
GET /ingame controllers.HomeController.ingame()
GET / controllers.HomeController.index()
GET /sessions controllers.HomeController.sessions()
GET /ingame/:id controllers.HomeController.ingame(id: String)
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB