work on game bookmarks

pull/1/merge
Thibault Duplessis 2012-06-08 02:19:21 +02:00
parent 76a512ecfd
commit 3f85628258
25 changed files with 237 additions and 76 deletions

View File

@ -2,17 +2,25 @@ package controllers
import lila._
import views._
import http.Context
import play.api.mvc.Result
object Game extends LilaController {
val gameRepo = env.game.gameRepo
val paginator = env.game.paginator
val cached = env.game.cached
val starApi = env.star.api
val listMenu = env.game.listMenu
val maxPage = 40
val realtime = Open { implicit ctx
IOk(gameRepo recentGames 9 map { games
html.game.realtime(games, cached.nbGames, cached.nbMates)
})
IOk(for {
games gameRepo recentGames 9
menu makeListMenu
} yield html.game.realtime(games, menu))
}
def realtimeInner(ids: String) = Open { implicit ctx
@ -22,20 +30,33 @@ object Game extends LilaController {
}
def all(page: Int) = Open { implicit ctx
(page < 50).fold(
Ok(html.game.all(
paginator recent page, cached.nbGames, cached.nbMates
)),
BadRequest("too old")
)
reasonable(page) {
IOk(makeListMenu map { menu
html.game.all(paginator recent page, menu)
})
}
}
def checkmate(page: Int) = Open { implicit ctx
(page < 50).fold(
Ok(html.game.checkmate(
paginator checkmate page, cached.nbGames, cached.nbMates
)),
BadRequest("too old")
)
reasonable(page) {
IOk(makeListMenu map { menu
html.game.checkmate(paginator checkmate page, menu)
})
}
}
def star(page: Int) = Auth { implicit ctx
me =>
reasonable(page) {
IOk(makeListMenu map { menu
html.game.star(starApi.gamePaginatorByUser(me, page), menu)
})
}
}
private def reasonable(page: Int)(result: Result): Result =
(page < maxPage).fold(result, BadRequest("too old"))
private def makeListMenu(implicit ctx: Context) =
listMenu(starApi.countByUser, ctx.me)
}

View File

@ -21,6 +21,7 @@ object Round extends LilaController {
private val messenger = env.round.messenger
private val rematcher = env.setup.rematcher
private val joiner = env.setup.friendJoiner
private val starApi = env.star.api
def websocketWatcher(gameId: String, color: String) = WebSocket.async[JsValue] { req
implicit val ctx = reqToCtx(req)
@ -41,8 +42,8 @@ object Round extends LilaController {
pov.game.started.fold(
messenger render pov.game map { roomHtml
Ok(html.round.player(
pov,
version(pov.gameId),
pov,
version(pov.gameId),
roomHtml map { Html(_) }))
},
io(Redirect(routes.Setup.await(fullId)))
@ -53,15 +54,19 @@ object Round extends LilaController {
def watcher(gameId: String, color: String) = Open { implicit ctx
IOptionIOResult(gameRepo.pov(gameId, color)) { pov
pov.game.started.fold(
io(Ok(html.round.watcher(pov, version(pov.gameId)))),
starApi usersByGame pov.game map { bookmarkers
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))
},
join(pov))
}
}
private def join(pov: Pov)(implicit ctx: Context): IO[Result] =
joiner(pov.game, ctx.me).fold(
err putFailures(err) map { _
Ok(html.round.watcher(pov, version(pov.gameId)))
err putFailures(err) flatMap { _
starApi usersByGame pov.game map { bookmarkers
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))
}
},
_ flatMap {
case (p, events) performEvents(p.gameId)(events) map { _

View File

@ -22,4 +22,6 @@ final class GameEnv(
maxPerPage = GamePaginatorMaxPerPage)
lazy val export = Export(gameRepo) _
lazy val listMenu = ListMenu(cached) _
}

View File

@ -0,0 +1,27 @@
package lila
package game
import user.User
import scalaz.effects._
case class ListMenu(
nbGames: Int,
nbMates: Int,
nbStars: Option[Int])
object ListMenu {
type CountStars = User IO[Int]
def apply(cached: Cached)(countStars: CountStars, me: Option[User]): IO[ListMenu] =
me.fold(
m countStars(m) map (_.some),
io(none)
) map { nbStars
new ListMenu(
nbGames = cached.nbGames,
nbMates = cached.nbMates,
nbStars = nbStars)
}
}

View File

@ -86,6 +86,8 @@ final class I18nKeys(translator: Translator) {
val gamesBeingPlayedRightNow = new Key("gamesBeingPlayedRightNow")
val viewAllNbGames = new Key("viewAllNbGames")
val viewNbCheckmates = new Key("viewNbCheckmates")
val nbBookmarks = new Key("nbBookmarks")
val bookmarkedByNbPlayers = new Key("bookmarkedByNbPlayers")
val viewInFullSize = new Key("viewInFullSize")
val logOut = new Key("logOut")
val signIn = new Key("signIn")

View File

@ -1,16 +1,27 @@
package lila
package star
import game.{ DbGame, GameRepo }
import game.DbGame
import user.{ User, UserRepo }
import scalaz.effects._
final class StarApi(
starRepo: StarRepo,
gameRepo: GameRepo,
userRepo: UserRepo) {
userRepo: UserRepo,
paginator: PaginatorBuilder) {
def starred(game: DbGame, user: User): IO[Boolean] =
starRepo.exists(game.id, user.id)
def countByUser(user: User): IO[Int] =
starRepo countByUserId user.id
def usersByGame(game: DbGame): IO[List[User]] = for {
userIds starRepo userIdsByGameId game.id
users (userIds map userRepo.byId).sequence
} yield users.flatten
def gamePaginatorByUser(user: User, page: Int) =
paginator.byUser(user: User, page: Int) map (_.game)
}

View File

@ -17,8 +17,14 @@ final class StarEnv(
lazy val starRepo = new StarRepo(mongodb(MongoCollectionStar))
lazy val api = new StarApi(
lazy val paginator = new PaginatorBuilder(
starRepo = starRepo,
gameRepo = gameRepo,
userRepo = userRepo)
userRepo = userRepo,
maxPerPage = GamePaginatorMaxPerPage)
lazy val api = new StarApi(
starRepo = starRepo,
userRepo = userRepo,
paginator = paginator)
}

View File

@ -26,6 +26,16 @@ final class StarRepo(val collection: MongoCollection) {
(collection count idQuery(gameId, userId)) > 0
}
def countByUserId(userId: String) = io {
collection count userIdQuery(userId) toInt
}
def userIdsByGameId(gameId: String): IO[List[String]] = io {
(collection find gameIdQuery(gameId) sort sortQuery() map { obj
obj.getAs[String]("u")
}).toList.flatten
}
def idQuery(gameId: String, userId: String) = DBObject("_id" -> (gameId + userId))
def gameIdQuery(gameId: String) = DBObject("g" -> gameId)
def userIdQuery(userId: String) = DBObject("u" -> userId)

View File

@ -16,6 +16,7 @@ object Environment
with SettingHelper
with ConfigHelper
with DateHelper
with NumberHelper
with JsonHelper
with PaginatorHelper
with FormHelper

View File

@ -0,0 +1,24 @@
package lila
package templating
import http.Context
import i18n.I18nHelper
import java.util.Locale
import java.text.NumberFormat
import scala.collection.mutable
trait NumberHelper { self: I18nHelper
private val formatters = mutable.Map[String, NumberFormat]()
private def formatter(ctx: Context): NumberFormat =
formatters.getOrElseUpdate(
lang(ctx).language,
NumberFormat getInstance new Locale(lang(ctx).language))
implicit def richInt(number: Int) = new {
def localize(implicit ctx: Context): String =
formatter(ctx) format number
}
}

View File

@ -35,4 +35,9 @@ trait StringHelper {
"<a href='%s'>%s</a>".format(m group 1, m group 1))
def showNumber(n: Int): String = (n > 0).fold("+" + n, n.toString)
implicit def richString(str: String) = new {
def active(other: String, then: String = "active") =
(str == other).fold(then, "")
}
}

View File

@ -1,4 +1,4 @@
@(title: String, active: Option[ui.SiteMenu.Elem] = None, baseline: Option[Html] = None, goodies: Option[Html] = None, chat: Option[Html] = None, robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
@(title: String, active: Option[ui.SiteMenu.Elem] = None, baseline: Option[Html] = None, goodies: Option[Html] = None, menu: Option[Html] = None, chat: Option[Html] = None, robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
<!doctype html>
<html lang="@lang.language">
@ -56,6 +56,9 @@
</a>
@baseline
</h1>
@menu.map { side =>
<div class="side_menu">@side</div>
}
@goodies.map { g =>
<div class="lichess_goodies_wrap">@g</div>
}

View File

@ -1,13 +1,7 @@
@(paginator: Paginator[DbGame], nbGames: Int, nbMates: Int)(implicit ctx: Context)
@menu = {
<a class="game_list" href="@routes.Game.realtime()">@trans.gamesBeingPlayedRightNow()</a>
<a class="game_list" href="@routes.Game.checkmate()">@trans.viewNbCheckmates(nbMates)</a>
}
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.list(
name = trans.viewAllNbGames.str(nbGames),
name = trans.viewAllNbGames.str(listMenu.nbGames.localize),
paginator = paginator,
next = routes.Game.all(paginator.nextPage | 1),
typ = trans.games.str(),
menu = menu)
next = paginator.nextPage map { n => routes.Game.all(n) },
menu = sideMenu(listMenu, "all"))

View File

@ -1,13 +1,7 @@
@(paginator: Paginator[DbGame], nbGames: Int, nbMates: Int)(implicit ctx: Context)
@menu = {
<a class="game_list" href="@routes.Game.realtime()">@trans.gamesBeingPlayedRightNow()</a>
<a class="game_list" href="@routes.Game.all()">@trans.viewAllNbGames(nbGames)</a>
}
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.list(
name = trans.viewNbCheckmates.str(nbMates),
name = trans.viewNbCheckmates.str(listMenu.nbMates.localize),
paginator = paginator,
next = routes.Game.all(paginator.nextPage | 1),
typ = trans.checkmate.str(),
menu = menu)
next = paginator.nextPage map { n => routes.Game.checkmate(n) },
menu = sideMenu(listMenu, "checkmate"))

View File

@ -1,7 +1,8 @@
@(title: String, goodies: Option[Html] = None, moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
@(title: String, goodies: Option[Html] = None, menu: Option[Html] = None, moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
@base.layout(
title = title,
goodies = goodies,
menu = menu,
active = siteMenu.game.some,
moreJs = moreJs)(body)

View File

@ -1,4 +1,4 @@
@(name: String, paginator: Paginator[DbGame], next: Call, typ: String, menu: Html)(implicit ctx: Context)
@(name: String, paginator: Paginator[DbGame], next: Option[Call], menu: Html)(implicit ctx: Context)
@title = @{ "%s - page %d".format(name, paginator.currentPage) }
@ -6,14 +6,18 @@
@jsTag("vendor/jquery.infinitescroll.min.js")
}
@game.layout(title = title, moreJs = moreJs) {
@game.layout(
title = title,
moreJs = moreJs,
menu = menu.some) {
<div class="content_box no_padding">
<div class="content_box_title">
<h1 class="title">@typ (@paginator.nbResults)</h1>
@menu
<h1 class="title">@name</h1>
</div>
<div class="all_games infinitescroll">
<div class="pager none"><a href="@next">Next</a></div>
@next.map { n =>
<div class="pager none"><a href="@n">Next</a></div>
}
@game.widgets(paginator.currentPageResults)
</div>
</div>

View File

@ -1,11 +1,11 @@
@(games: List[DbGame], nbGames: Int, nbMates: Int)(implicit ctx: Context)
@(games: List[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.layout(title = trans.gamesBeingPlayedRightNow.str()) {
@game.layout(
title = trans.gamesBeingPlayedRightNow.str(),
menu = sideMenu(listMenu, "realtime").some) {
<div class="content_box current_games_box">
<h1 class="title">@trans.gamesBeingPlayedRightNow()</h1>
<a class="all_games" href="@routes.Game.all()">@trans.viewAllNbGames(nbGames)</a>
<a class="all_games" href="@routes.Game.checkmate()">@trans.viewNbCheckmates(nbMates)</a>
<div class="game_list" data-url="@routes.Game.realtimeInner(games.map(_.id).mkString(","))">
<div class="game_list realtime" data-url="@routes.Game.realtimeInner(games.map(_.id).mkString(","))">
@game.realtimeInner(games)
</div>
</div>

View File

@ -0,0 +1,16 @@
@(listMenu: lila.game.ListMenu, active: String)(implicit ctx: Context)
<a class="@active.active("realtime")" href="@routes.Game.realtime()">
@trans.gamesBeingPlayedRightNow()
</a>
<a class="@active.active("all")" href="@routes.Game.all()">
@trans.viewAllNbGames(listMenu.nbGames.localize)
</a>
<a class="@active.active("checkmate")" href="@routes.Game.checkmate()">
@trans.viewNbCheckmates(listMenu.nbMates.localize)
</a>
@listMenu.nbStars.map { nb =>
<a class="@active.active("star")" href="@routes.Game.star()">
@trans.nbBookmarks(nb.localize)
</a>
}

View File

@ -0,0 +1,7 @@
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.list(
name = trans.nbBookmarks.str(listMenu.nbStars.fold(_.localize, 0)),
paginator = paginator,
next = paginator.nextPage map { n => routes.Game.star(n) },
menu = sideMenu(listMenu, "star"))

View File

@ -1,4 +1,4 @@
@(pov: Pov, version: Int)(implicit ctx: Context)
@(pov: Pov, version: Int, bookmarkers: List[User])(implicit ctx: Context)
@import pov._
@ -40,8 +40,16 @@
}
<br /><br />
<a class="rotate_board" href="@routes.Round.watcher(gameId, opponent.color.name)">@trans.flipBoard()</a>
<br /><br />
<a href="@routes.Lobby.home()"><strong>@trans.playANewGame()</strong></a>
@if(bookmarkers.nonEmpty) {
<div class="bookmarkers">
<p>@trans.bookmarkedByNbPlayers(bookmarkers.size)</p>
<ul>
@bookmarkers.map { bookmarker =>
<li>@userLink(bookmarker)</li>
}
</ul>
</div>
}
</div>
}

View File

@ -23,7 +23,6 @@ random=Random
noGameAvailableRightNowCreateOne=No game available right now, create one!
whiteIsVictorious=White is victorious
blackIsVictorious=Black is victorious
playANewGame=Play a new game
rematch=Rematch
playWithTheSameOpponentAgain=Play with the same opponent again
newOpponent=New opponent
@ -59,9 +58,11 @@ draw=Draw
nbConnectedPlayers=%s connected players
talkAboutChessAndDiscussLichessFeaturesInTheForum=Talk about chess and discuss lichess features in the forum
seeTheGamesBeingPlayedInRealTime=See the games being played in real time
gamesBeingPlayedRightNow=Games being played right now
viewAllNbGames=View all %s games
viewNbCheckmates=View %s checkmates
gamesBeingPlayedRightNow=Current Games
viewAllNbGames=%s Games
viewNbCheckmates=%s Checkmates
nbBookmarks=%s Bookmarks
bookmarkedByNbPlayers=Bookmarked by %s players
viewInFullSize=View in full size
logOut=Log out
signIn=Sign in

View File

@ -6,6 +6,7 @@ GET /games controllers.Game.realtime
GET /games/refresh/:ids controllers.Game.realtimeInner(ids: String)
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
GET /games/bookmark controllers.Game.star(page: Int ?= 1)
# Round
GET /$gameId<[\w\-]{8}> controllers.Round.watcher(gameId: String, color: String = "white")

View File

@ -210,6 +210,32 @@ div.lichess_goodies div.box {
div.lichess_goodies div.box .player {
margin: 4px 0;
}
div.bookmarkers {
margin-top: 1em;
}
div.bookmarkers li {
margin-top: 5px;
}
div.side_menu {
margin-top: 15px;
}
div.side_menu a {
padding: 8px 0 8px 8px;
display: block;
text-decoration: none;
width: 203px;
font-weight: bold;
}
div.side_menu a.active {
background: white;
border: 1px solid #dadada;
border-right: none;
border-radius: 4px 0 0 4px;
box-shadow: -3px 0 6px #d0d0d0;
}
#nb_connected_players, a.goto_nav, #top a.toggle, a#sound_state {
text-decoration: none;
font-size: 13px;

View File

@ -9,18 +9,6 @@ div.current_games_box {
width: 638px;
}
a.game_list, a.all_games {
margin-left: 2em;
}
div.game_list {
margin-top: 2em;
}
div.all_games {
margin-top: 1em;
}
div.game_row {
display: block;
padding: 1.4em 20px;
@ -60,6 +48,9 @@ div.game_row span.black {
background-position: 0 -256px;
}
div.game_list.realtime {
margin-top: 1.4em;
}
div.game_list_inner > div {
float: left;
margin: 0 10px 10px 10px;

1
todo
View File

@ -23,6 +23,7 @@ guess friend list
star people and games (and forum threads?)
autoclose top menus
tournaments http://www.chess.com/tournaments/help.html
long name display issue http://en.lichess.org/forum/lichess-feedback/long-names-alter-layout
new translations:
-rematchOfferCanceled=Rematch offer canceled