streamer show UI

more-scalatags
Thibault Duplessis 2019-03-22 14:43:37 +07:00
parent ae5e08a4ff
commit d07da0e3e4
14 changed files with 314 additions and 283 deletions

View File

@ -20,6 +20,12 @@ trait ScalatagsAttrs {
lazy val dataColor = attr("data-color")
lazy val dataFen = attr("data-fen")
lazy val novalidate = attr("novalidate")
object frame {
val frameborder = attr("frameborder")
val scrolling = attr("scrolling")
val allowfullscreen = attr("allowfullscreen")
val autoplay = attr("autoplay")
}
}
// collection of lila snippets

View File

@ -28,7 +28,7 @@ object bits {
}
def menu(active: String, s: Option[lila.streamer.Streamer.WithUser])(implicit ctx: Context) =
st.nav(cls := "page-menu__menu subnav")(
st.nav(cls := "subnav")(
a(cls := active.active("index"), href := routes.Streamer.index())("All streamers"),
s.map { st =>
frag(

View File

@ -0,0 +1,70 @@
package views.html.streamer
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
object header {
def apply(s: lila.streamer.Streamer.WithUserAndStream, following: Option[Boolean])(implicit ctx: Context) =
div(cls := "top")(
bits.pic(s.streamer, s.user, 300),
div(cls := "overview")(
h1(
titleTag(s.user.title),
s.streamer.name
),
s.streamer.headline.map(_.value).map { d =>
p(cls := s"headline ${if (d.size < 60) "small" else if (d.size < 120) "medium" else "large"}")(d)
},
div(cls := "services")(
s.streamer.twitch.map { twitch =>
a(
cls := List(
"service twitch" -> true,
"live" -> s.stream.exists(_.twitch)
),
href := twitch.fullUrl
)(bits.svg.twitch, " ", twitch.minUrl)
},
s.streamer.youTube.map { youTube =>
a(
cls := List(
"service youTube" -> true,
"live" -> s.stream.exists(_.twitch)
),
href := youTube.fullUrl
)(bits.svg.youTube, " ", youTube.minUrl)
},
a(cls := "service lichess", href := routes.User.show(s.user.username))(
bits.svg.lichess,
" ",
s"lichess.org/@/${s.user.username}"
)
),
div(cls := "ats")(
s.stream.map { s =>
p(cls := "at")(
"Currently streaming: ",
strong(s.status)
)
} getOrElse frag(
p(cls := "at")(trans.lastSeenActive.frag(momentFromNow(s.streamer.seenAt))),
s.streamer.liveAt.map { liveAt =>
p(cls := "at")("Last stream ", momentFromNow(liveAt))
}
)
),
following.map { f =>
(ctx.isAuth && !ctx.is(s.user)) option
button(attr("data-user") := s.user.id, cls := List(
"follow icon button" -> true,
"active" -> f
), tpe := "submit")(
span(cls := "text", dataIcon := "h")(trans.follow.frag())
)
}
)
)
}

View File

@ -1,50 +0,0 @@
@(s: lila.streamer.Streamer.WithUserAndStream, following: Option[Boolean])(implicit ctx: Context)
<div class="top">
@bits.pic(s.streamer, s.user, 250)
<div class="overview">
<h1>@titleTag(s.user.title)@s.streamer.name</h1>
@s.streamer.headline.map(_.value).map { d =>
<p class="headline @if(d.size < 60){small} else {@if(d.size < 120){medium}else{large}}">@d</p>
}
<div class="services">
@s.streamer.twitch.map { twitch =>
<a class="service twitch@if(s.stream.exists(_.twitch)){ live}" href="@twitch.fullUrl">
@bits.svg.twitch
@twitch.minUrl
</a>
}
@s.streamer.youTube.map { youTube =>
<a class="service youTube@if(s.stream.exists(_.youTube)){ live}" href="@youTube.fullUrl">
@bits.svg.youTube
@youTube.minUrl
</a>
}
<a class="service lichess" href="@routes.User.show(s.user.username)">
@bits.svg.lichess
lichess.org/@@/@s.user.username
</a>
</div>
<div class="metas">
<div class="ats">
@s.stream.map { s =>
<p class="at">
Currently streaming:<br />
<strong>@s.status</strong>
</p>
}.getOrElse {
<p class="at">@trans.lastSeenActive(momentFromNow(s.streamer.seenAt))</p>
@s.streamer.liveAt.map { liveAt =>
<p class="at">Last stream @momentFromNow(liveAt)</p>
}
}
</div>
@following.map { f =>
@if(ctx.isAuth && !ctx.is(s.user)) {
<button data-user="@s.user.id" class="follow icon button@if(f){ active}" type="submit">
<span class="text" data-icon="h">@trans.follow()</span>
</button>
}
}
</div>
</div>
</div>

View File

@ -1,7 +1,5 @@
package views.html.streamer
import play.twirl.api.Html
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
@ -74,7 +72,7 @@ object index {
moreJs = infiniteScrollTag
) {
main(cls := "page-menu")(
bits.menu("index", none),
bits.menu("index", none)(ctx)(cls := " page-menu__menu"),
div(cls := "page-menu__content box streamer-list")(
h1(dataIcon := "", cls := "text")(title),
!requests option div(cls := "list live")(

View File

@ -0,0 +1,98 @@
package views.html.streamer
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.richText
import lila.streamer.Stream.{ Twitch, YouTube }
object show {
def apply(
s: lila.streamer.Streamer.WithUserAndStream,
activities: Vector[lila.activity.ActivityView],
following: Boolean
)(implicit ctx: Context) = views.html.base.layout(
title = s"${s.titleName} streams chess",
responsive = true,
moreCss = responsiveCssTag("streamer.show"),
moreJs = embedJs("""
$(function() {
$('button.follow').click(function() {
var klass = 'active';
$(this).toggleClass(klass);
$.ajax({
url: '/rel/' + ($(this).hasClass('active') ? 'follow/' : 'unfollow/') + $(this).data('user'),
method:'post'
});
});
});"""),
openGraph = lila.app.ui.OpenGraph(
title = s"${s.titleName} streams chess",
description = shorten(~(s.streamer.headline.map(_.value) orElse s.streamer.description.map(_.value)), 152),
url = s"$netBaseUrl${routes.Streamer.show(s.user.username)}",
`type` = "video",
image = s.streamer.picturePath.map(p => dbImageUrl(p.value))
).some,
csp = defaultCsp.withTwitch.some
)(
main(cls := "page-menu streamer-show")(
st.aside(cls := "page-menu__menu")(
s.streamer.approval.chatEnabled option div(cls := "streamer-chat")(
s.stream match {
case Some(YouTube.Stream(_, _, videoId, _)) => iframe(
frame.frameborder := "0",
frame.scrolling := "no",
src := s"https://www.youtube.com/live_chat?v=$videoId&embed_domain=$netDomain"
)
case _ => s.streamer.twitch.map { twitch =>
iframe(
frame.frameborder := "0",
frame.scrolling := "yes",
src := s"https://twitch.tv/embed/${twitch.userId}/chat${(ctx.currentBg != "light") ?? "?darkpopout"}"
)
}
}
),
bits.menu("show", s.withoutStream.some),
a(cls := "blocker button button-metal", href := "https://getublockorigin.com")(
i(dataIcon := ""),
strong("Install a malware blocker!"),
"Be safe from ads and trackers", br,
"infesting Twitch and YouTube.", br,
"Lichess recommend uBlock Origin", br,
"which is free and open-source."
)
),
div(cls := "page-menu__content")(
s.stream match {
case Some(YouTube.Stream(_, _, videoId, _)) => div(cls := "box embed youTube")(
iframe(
src := s"https://www.youtube.com/embed/$videoId?autoplay=1",
frame.frameborder := "0",
frame.allowfullscreen := true
)
)
case _ => s.streamer.twitch.map { twitch =>
div(cls := "box embed twitch")(
iframe(
src := s"https://player.twitch.tv/?channel=${twitch.userId}",
frame.allowfullscreen := true,
frame.autoplay := true
)
)
} getOrElse div(cls := "box embed")(div(cls := "nostream")("OFFLINE"))
},
div(cls := "box streamer")(
header(s, following.some),
div(cls := "description")(richText(s.streamer.description.fold("")(_.value))),
a(cls := "ratings", href := routes.User.show(s.user.username))(
s.user.best6Perfs.map { showPerfRating(s.user, _) }
),
views.html.activity(s.user, activities)
)
)
)
)
}

View File

@ -1,110 +0,0 @@
@(s: lila.streamer.Streamer.WithUserAndStream, activities: Vector[lila.activity.ActivityView], following: Boolean)(implicit ctx: Context)
@import lila.streamer.Stream.{ Twitch, YouTube }
@side = {
@if(s.streamer.approval.chatEnabled) {
<div class="side streamer-side">
@s.stream match {
case Some(YouTube.Stream(_, _, videoId, _)) => {
<iframe
frameborder="0"
scrolling="no"
src="https://www.youtube.com/live_chat?v=@videoId&embed_domain=@netDomain"
width="235"
height="500"></iframe>
}
case _ => {
@s.streamer.twitch.map { twitch =>
<iframe
frameborder="0"
scrolling="yes"
src="https://twitch.tv/embed/@twitch.userId/chat@if(ctx.currentBg != "light"){?darkpopout}"
width="235"
height="500"></iframe>
}
}
}
</div>
}
<div class="side_menu">
@bits.menu("show", s.withoutStream.some).toHtml
</div>
<a class="blocker button" href="https://getublockorigin.com">
<i data-icon=""></i>
<strong>Install a malware blocker!</strong>
Be safe from ads and trackers<br />
infesting Twitch and YouTube.<br />
We recommend uBlock Origin<br />
which is free and open-source.
</a>
}
@moreCss = {
@cssTag("streamer.show.css")
@cssTag("activity.css")
}
@moreJs = {
@embedJs {
$(function() {
$('button.follow').click(function() {
var klass = 'active';
$(this).toggleClass(klass);
$.ajax({
url: '/rel/' + ($(this).hasClass('active') ? 'follow/' : 'unfollow/') + $(this).data('user'),
method:'post'
});
});
});
}
}
@title = @{ s"${s.titleName} streams chess" }
@base.layout(title = title,
side = side.some,
moreCss = moreCss,
moreJs = moreJs,
openGraph = lila.app.ui.OpenGraph(
title = title,
description = shorten(~(s.streamer.headline.map(_.value) orElse s.streamer.description.map(_.value)), 152),
url = s"$netBaseUrl${routes.Streamer.show(s.user.username)}",
`type` = "video",
image = s.streamer.picturePath.map(p => dbImageUrl(p.value))).some,
csp = defaultCsp.withTwitch.some) {
@s.stream match {
case Some(YouTube.Stream(_, _, videoId, _)) => {
<div class="content_box no_padding livestream youTube">
<iframe
width="792"
height="446"
src="https://www.youtube.com/embed/@videoId?autoplay=1"
frameborder="0" allowfullscreen></iframe>
</div>
}
case _ => {
@s.streamer.twitch.map { twitch =>
<div class="content_box no_padding livestream twitch">
<iframe
src="https://player.twitch.tv/?channel=@twitch.userId"
width="792"
height="446"
allowfullscreen autoplay></iframe>
</div>
}.getOrElse {
<div class="content_box no_padding nostream">OFFLINE</div>
}
}
}
<div class="content_box no_padding streamer">
@header(s, following.some)
<div class="description">@richText(s.streamer.description.fold("")(_.value))</div>
<a class="ratings" href="@routes.User.show(s.user.username)">
@s.user.best6Perfs.map { pt =>
@showPerfRating(s.user, pt)
}
</a>
@views.html.activity(s.user, activities).toHtml
</div>
}.toHtml

View File

@ -1,119 +0,0 @@
.streamer-side {
margin: 15px 0 0 -35px;
}
.streamer .top {
display: flex;
}
.streamer img.picture {
flex: 0 0 250px;
}
.streamer .overview {
margin: 20px 10px 0 20px;
display: flex;
flex-flow: column;
justify-content: space-between;
width: 100%;
}
.streamer .metas {
display: flex;
justify-content: space-between;
align-content: center;
}
.streamer .follow:disabled {
cursor: default;
opacity: 1;
}
div.content_box.streamer h1 {
font-family: 'Roboto';
font-size: 32px;
text-transform: uppercase;
letter-spacing: 4px;
padding: 0!important;
text-shadow: none!important;
}
.streamer .headline {
font-family: 'PT Serif', 'Noto Sans', 'Lucida Grande';
font-size: 17px;
padding: 0!important;
}
.streamer .headline.medium {
font-size: 15px;
}
.streamer .headline.large {
font-size: 14px;
}
.streamer .services {
margin: 5px 0;
}
.streamer .service {
display: flex;
font-size: 1.5em;
white-space: nowrap;
padding: 5px 0;
}
.streamer .service.live {
color: #dc322f;
}
.streamer .service svg {
width: 1.4em;
height: 1.4em;
margin-right: 0.4em;
}
.streamer a.service:hover svg path {
fill: #3893E8!important;
}
.livestream {
margin-bottom: 0px;
line-height: 0;
margin-bottom: 10px;
}
.livestream iframe {
border: none;
}
.nostream {
height: 300px;
background: black;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
letter-spacing: 10px;
}
.streamer .description {
padding: 2.5em 50px 2.5em 65px;
font-size: 1.4em;
}
.streamer .services a:hover,
.streamer .description a {
color: #3893E8;
}
.streamer .ratings {
padding: 2em 50px 2em 58px;
font-size: 1.6em;
background: rgba(213,145,32,0.2);
display: flex;
justify-content: space-between;
margin-bottom: 1em;
}
.streamer .ratings span::before {
font-size: 1.6em;
margin-right: 0.1em;
}
.blocker {
margin-top: 30px;
padding: 15px 0!important;
display: flex;
flex-flow: column;
align-items: center;
text-align: center;
font-weight: normal!important;
}
.blocker i {
font-size: 40px;
margin-bottom: 10px;
}
body.dark .streamer .service svg path {
fill: #aaa;
}

View File

@ -0,0 +1,3 @@
@import '../../../common/css/plugin';
@import '../user/activity';
@import '../streamer/show';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/dark';
@import 'streamer.show';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/light';
@import 'streamer.show';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/transp';
@import 'streamer.show';

View File

@ -0,0 +1,129 @@
.streamer-show {
.streamer-chat {
margin-right: $block-gap;
display: none;
@include breakpoint($mq-small) {
display: block;
}
iframe {
height: 500px;
}
}
.top {
display: flex;
}
.picture {
flex: 0 0 300px;
}
.overview {
margin: 20px 10px 0 20px;
display: flex;
flex-flow: column;
justify-content: space-between;
width: 100%;
}
.metas {
display: flex;
justify-content: space-between;
align-content: center;
}
.follow:disabled {
cursor: default;
opacity: 1;
}
h1 {
@extend %roboto;
font-size: 2.2rem;
text-transform: uppercase;
letter-spacing: 4px;
margin-bottom: .7em;
}
.headline {
font-style: italic;
}
.headline.medium {
font-size: .95em;
}
.headline.large {
font-size: .9em;
}
.services {
margin: 5px 0;
}
.service {
display: flex;
font-size: 1.2em;
white-space: nowrap;
padding: 5px 0;
}
.service.live {
color: $c-brag;
}
.service svg {
width: 1.4em;
height: 1.4em;
margin-right: 0.4em;
}
a.service:hover svg path {
fill: $c-link;
}
.embed {
position: relative;
padding-bottom: 56.25%;
height: 0;
min-height: 0;
overflow: hidden;
max-width: 100%;
margin-bottom: $block-gap;
}
.embed > * {
@extend %abs-100;
top: 0;
left: 0;
border: none;
}
.nostream {
background: black;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
letter-spacing: 10px;
}
.description {
padding: 2.5em 50px 2.5em 65px;
font-size: 1.4em;
}
.services a:hover,
.description a {
color: #3893E8;
}
.ratings {
padding: 2em 50px 2em 58px;
font-size: 1.6em;
line-height: .9;
background: mix($c-brag, $c-bg-box, 20%);
color: $c-font;
display: flex;
justify-content: space-between;
margin-bottom: 1em;
span::before {
font-size: 1.6em;
margin-right: 0.1em;
}
}
.blocker {
margin: $block-gap $block-gap 0 0;
display: flex;
flex-flow: column;
align-items: center;
text-align: center;
font-weight: normal;
text-transform: none;
}
.blocker i {
font-size: 40px;
margin-bottom: 10px;
}
}

View File