From 153e084f96fc30aebf0ff29511f65d356081f8a4 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 18 Dec 2024 13:12:21 -0600 Subject: [PATCH 01/18] swap out image link for default embed --- app/controllers/RelayRound.scala | 17 +-- modules/relay/src/main/JsonView.scala | 8 +- .../relay/src/main/RelayPinnedStream.scala | 19 ++- modules/relay/src/main/RelayTourForm.scala | 4 +- modules/relay/src/main/RelayVideoEmbed.scala | 10 +- modules/relay/src/main/ui/RelayFormUi.scala | 11 ++ ui/analyse/css/study/relay/_layout.scss | 1 - ui/analyse/css/study/relay/_tour.scss | 3 + ui/analyse/css/study/relay/_video-player.scss | 57 ++++++++- ui/analyse/src/analyse.ts | 5 +- ui/analyse/src/interfaces.ts | 2 + ui/analyse/src/plugins/analyse.study.ts | 12 +- ui/analyse/src/study/relay/interfaces.ts | 2 +- ui/analyse/src/study/relay/relayCtrl.ts | 36 ++++-- ui/analyse/src/study/relay/relayTourView.ts | 40 ++++--- ui/analyse/src/study/relay/relayView.ts | 3 +- ui/analyse/src/study/relay/videoPlayer.ts | 110 ++++++++++++------ 17 files changed, 230 insertions(+), 110 deletions(-) diff --git a/app/controllers/RelayRound.scala b/app/controllers/RelayRound.scala index 8d90f4443551d..00a6d1a388cb8 100644 --- a/app/controllers/RelayRound.scala +++ b/app/controllers/RelayRound.scala @@ -268,20 +268,21 @@ final class RelayRound( isSubscribed <- ctx.me.soFu: me => env.relay.api.isSubscribed(rt.tour.id, me.userId) videoUrls <- embed match - case VideoEmbed.Auto => - fuccess: - rt.tour.pinnedStream - .ifFalse(rt.round.isFinished) - .flatMap(_.upstream) - .map(_.urls(netDomain).toPair) - case VideoEmbed.No => fuccess(none) case VideoEmbed.Stream(userId) => env.streamer.api .find(userId) .flatMapz(s => env.streamer.liveStreamApi.of(s).dmap(some)) .map: _.flatMap(_.stream).map(_.urls(netDomain).toPair) - crossSiteIsolation = videoUrls.isEmpty + case VideoEmbed.PinnedStream => + fuccess: + rt.tour.pinnedStream + .ifFalse(rt.round.isFinished) + .flatMap(_.upstream) + .map(_.urls(netDomain).toPair) + case _ => fuccess(none) + crossSiteIsolation = videoUrls.isEmpty || (rt.tour.pinnedStream.isDefined && crossOriginPolicy + .supportsCredentiallessIFrames(ctx.req)) data = env.relay.jsonView.makeData( rt.tour.withRounds(rounds.map(_.round)), rt.round.id, diff --git a/modules/relay/src/main/JsonView.scala b/modules/relay/src/main/JsonView.scala index a7bf9e8a646b2..534f33bc8c36f 100644 --- a/modules/relay/src/main/JsonView.scala +++ b/modules/relay/src/main/JsonView.scala @@ -128,8 +128,12 @@ final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup, picfitUrl: PicfitUrl .add("lcc", trs.rounds.find(_.id == currentRoundId).map(_.sync.upstream.exists(_.hasLcc))) .add("isSubscribed" -> isSubscribed) .add("videoUrls" -> videoUrls) - .add("pinnedStream" -> pinned) - .add("note" -> trs.tour.note.ifTrue(canContribute)), + .add("note" -> trs.tour.note.ifTrue(canContribute)) + .add("pinned" -> pinned.map: p => + Json + .obj("name" -> p.name) + .add("redirect" -> p.upstream.map(_.urls(lila.core.config.NetDomain("")).redirect)) + .add("text" -> p.text)), study = studyData.study, analysis = studyData.analysis, group = group.map(_.group.name) diff --git a/modules/relay/src/main/RelayPinnedStream.scala b/modules/relay/src/main/RelayPinnedStream.scala index 5c27cddb858ae..73427117a03ff 100644 --- a/modules/relay/src/main/RelayPinnedStream.scala +++ b/modules/relay/src/main/RelayPinnedStream.scala @@ -4,22 +4,19 @@ import io.mola.galimatias.URL import scala.jdk.CollectionConverters.* import lila.core.config.NetDomain -case class RelayPinnedStream(name: String, url: URL): +case class RelayPinnedStream(name: String, url: URL, text: Option[String]): import RelayPinnedStream.* def upstream: Option[RelayPinnedStream.Upstream] = parseYoutube.orElse(parseTwitch) - // https://www.youtube.com/live/Lg0askmGqvo - // https://www.youtube.com/live/Lg0askmGqvo?si=KKOexnmA2xPcyStZ def parseYoutube: Option[YouTube] = - url.host.toString - .endsWith("youtube.com") - .so: - url.pathSegments.asScala.toList match - case List("live", id) => YouTube(id).some - case _ => none + if List("www.youtube.com", "youtube.com", "youtu.be").contains(url.host.toString) then + url.pathSegments.asScala.toList match + case List("live", id) => Some(YouTube(id)) + case _ => Option(url.queryParameter("v")).map(YouTube.apply) + else None // https://www.twitch.tv/tcec_chess_tv def parseTwitch: Option[Twitch] = @@ -37,11 +34,11 @@ object RelayPinnedStream: def urls(parent: NetDomain): Urls case class YouTube(id: String) extends Upstream: def urls(parent: NetDomain) = Urls( - s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1", + s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1&autoplay=1", s"https://www.youtube.com/watch?v=${id}" ) case class Twitch(id: String) extends Upstream: def urls(parent: NetDomain) = Urls( - s"https://player.twitch.tv/?channel=${id}&parent=${parent}", + s"https://player.twitch.tv/?channel=${id}&parent=${parent}&autoplay=true", s"https://www.twitch.tv/${id}" ) diff --git a/modules/relay/src/main/RelayTourForm.scala b/modules/relay/src/main/RelayTourForm.scala index b32792c96f1d8..95eb2eea4b07b 100644 --- a/modules/relay/src/main/RelayTourForm.scala +++ b/modules/relay/src/main/RelayTourForm.scala @@ -35,7 +35,9 @@ final class RelayTourForm(langList: lila.core.i18n.LangList): private val pinnedStreamMapping = mapping( "name" -> cleanNonEmptyText(maxLength = 100), - "url" -> url.field.verifying("Invalid stream URL", url => RelayPinnedStream("", url).upstream.isDefined) + "url" -> url.field + .verifying("Invalid stream URL", url => RelayPinnedStream("", url, None).upstream.isDefined), + "text" -> optional(cleanText(maxLength = 100)) )(RelayPinnedStream.apply)(unapply) private given Formatter[RelayTour.Tier] = diff --git a/modules/relay/src/main/RelayVideoEmbed.scala b/modules/relay/src/main/RelayVideoEmbed.scala index 4d7886b1bd795..f8e1392f56c99 100644 --- a/modules/relay/src/main/RelayVideoEmbed.scala +++ b/modules/relay/src/main/RelayVideoEmbed.scala @@ -7,11 +7,11 @@ import play.api.mvc.Result enum RelayVideoEmbed: case No case Auto + case PinnedStream case Stream(userId: UserId) override def toString = this match - case No => "no" - case Auto => "" - case Stream(u) => u.toString + case No => "no" + case _ => "" final class RelayVideoEmbedStore(baker: LilaCookie): @@ -21,11 +21,11 @@ final class RelayVideoEmbedStore(baker: LilaCookie): def read(using req: RequestHeader): RelayVideoEmbed = def fromCookie = req.cookies.get(cookieName).map(_.value).filter(_.nonEmpty) match case Some("no") => No + case Some("ps") => PinnedStream case _ => Auto req.queryString.get("embed") match - case Some(Nil) => fromCookie - case Some(Seq("")) => Auto case Some(Seq("no")) => No + case Some(Seq("ps")) => PinnedStream case Some(Seq(name)) => UserStr.read(name).fold(Auto)(u => Stream(u.id)) case _ => fromCookie diff --git a/modules/relay/src/main/ui/RelayFormUi.scala b/modules/relay/src/main/ui/RelayFormUi.scala index e3d58d352a7f6..735244ef43d9b 100644 --- a/modules/relay/src/main/ui/RelayFormUi.scala +++ b/modules/relay/src/main/ui/RelayFormUi.scala @@ -631,6 +631,17 @@ Team Dogs ; Scooby Doo"""), "Stream name", half = true )(form3.input(_)) + ), + form3.split( + form3.group( + form("pinnedStream.text"), + "Stream link label", + help = frag( + "Optional. Show a label on the image link to your live stream.", + br, + "Example: 'Watch us live on YouTube!'" + ).some + )(form3.input(_)) ) ) ) diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index d23bea278cd56..f6c9fdc79f63b 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -22,7 +22,6 @@ main.is-relay { } @include mq-at-least-col3 { - #video-player-placeholder, button.streamer-show { display: block; } diff --git a/ui/analyse/css/study/relay/_tour.scss b/ui/analyse/css/study/relay/_tour.scss index 331c54efdd448..039efc9d0162f 100644 --- a/ui/analyse/css/study/relay/_tour.scss +++ b/ui/analyse/css/study/relay/_tour.scss @@ -121,6 +121,9 @@ $hover-bg: $m-primary_bg--mix-30; width: 100%; @include broken-img(2 / 1); } + .video-player-close { + display: none; + } text-align: center; } &__image-upload { diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 9d3019de23f5f..8d8c27623bb3c 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -7,16 +7,65 @@ #video-player-placeholder { aspect-ratio: 16/9; + position: relative; width: 100%; } -img.video-player-close { +.video-player-close { z-index: $z-video-player-controls-101; position: absolute; - height: 20px; - width: 20px; + pointer-events: auto; + top: 6px; + right: 6px; + height: 24px; + width: 24px; + padding: 2px; + border-radius: 50%; + background-color: black; cursor: pointer; &:hover { - filter: brightness(10); + filter: brightness(3); + } +} + +#video-player-placeholder.link { + cursor: pointer; + + .image { + object-fit: cover; + width: 100%; + height: 100%; + } + .play-button { + position: absolute; + pointer-events: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0.6; + } + .text { + @extend %flex-column; + position: absolute; + justify-content: center; + align-items: center; + bottom: 5%; + left: 10%; + right: 10%; + pointer-events: none; + } + .text div { + margin: auto; + pointer-events: none; + border-radius: 5px; + border: 1px solid #888; + padding: 5px; + text-align: center; + line-height: normal; + color: #222e; + background-color: #dddd; + } + &:hover:not(:has(.video-player-close:hover)) .play-button { + opacity: 1; } } diff --git a/ui/analyse/src/analyse.ts b/ui/analyse/src/analyse.ts index 2131f0ea27fbc..04f68cf94250d 100644 --- a/ui/analyse/src/analyse.ts +++ b/ui/analyse/src/analyse.ts @@ -5,9 +5,8 @@ import { wsConnect } from 'common/socket'; export { patch }; -export const start = makeStart(patch); - -export const boot = makeBoot(start); +const start = makeStart(patch); +const boot = makeBoot(start); export function initModule({ mode, cfg }: { mode: 'userAnalysis' | 'replay'; cfg: any }) { if (mode === 'replay') boot(cfg); diff --git a/ui/analyse/src/interfaces.ts b/ui/analyse/src/interfaces.ts index 94eb304a0abc6..74ef61457b810 100644 --- a/ui/analyse/src/interfaces.ts +++ b/ui/analyse/src/interfaces.ts @@ -156,6 +156,8 @@ export interface AnalyseOpts { inlinePgn?: string; externalEngineEndpoint: string; embed?: boolean; + socketUrl?: string; + socketVersion?: number; } export interface JustCaptured extends Piece { diff --git a/ui/analyse/src/plugins/analyse.study.ts b/ui/analyse/src/plugins/analyse.study.ts index e793576789599..efae622bbac16 100644 --- a/ui/analyse/src/plugins/analyse.study.ts +++ b/ui/analyse/src/plugins/analyse.study.ts @@ -1,18 +1,18 @@ import { patch } from '../view/util'; -import makeBoot from '../boot'; import makeStart from '../start'; +import type { AnalyseOpts } from '../interfaces'; +import type { AnalyseSocketSend } from '../socket'; import * as studyDeps from '../study/studyDeps'; import { wsConnect } from 'common/socket'; export { patch }; -export const start = makeStart(patch, studyDeps); -export const boot = makeBoot(start); +const start = makeStart(patch, studyDeps); -export function initModule(cfg: any) { - cfg.socketSend = wsConnect(cfg.socketUrl || '/analysis/socket/v5', cfg.socketVersion, { +export function initModule(cfg: AnalyseOpts) { + cfg.socketSend = wsConnect(cfg.socketUrl || '/analysis/socket/v5', cfg.socketVersion ?? false, { receive: (t: string, d: any) => analyse.socketReceive(t, d), ...(cfg.embed ? { params: { flag: 'embed' } } : {}), - }).send; + }).send as AnalyseSocketSend; const analyse = start(cfg); } diff --git a/ui/analyse/src/study/relay/interfaces.ts b/ui/analyse/src/study/relay/interfaces.ts index ddf0ab794f2fe..aebc0d3bdac55 100644 --- a/ui/analyse/src/study/relay/interfaces.ts +++ b/ui/analyse/src/study/relay/interfaces.ts @@ -5,7 +5,7 @@ export interface RelayData { group?: RelayGroup; isSubscribed?: boolean; // undefined if anon videoUrls?: [string, string]; - pinnedStream?: { name: string; youtube?: string; twitch?: string }; + pinned?: { name: string; redirect: string; text?: string }; note?: string; lcc?: boolean; } diff --git a/ui/analyse/src/study/relay/relayCtrl.ts b/ui/analyse/src/study/relay/relayCtrl.ts index 77f8aee390370..3e69c95712b31 100644 --- a/ui/analyse/src/study/relay/relayCtrl.ts +++ b/ui/analyse/src/study/relay/relayCtrl.ts @@ -2,7 +2,7 @@ import type { RelayData, LogEvent, RelaySync, RelayRound, RoundId } from './inte import type { BothClocks, ChapterId, ChapterSelect, Federations, ServerClockMsg } from '../interfaces'; import type { StudyMemberCtrl } from '../studyMembers'; import type { AnalyseSocketSend } from '../../socket'; -import { type Prop, type Toggle, defined, myUserId, notNull, prop, toggle } from 'common'; +import { type Prop, type Toggle, myUserId, notNull, prop, toggle } from 'common'; import RelayTeams from './relayTeams'; import RelayPlayers from './relayPlayers'; import type { StudyChapters } from '../studyChapters'; @@ -59,19 +59,27 @@ export default class RelayCtrl { redraw, ); this.stats = new RelayStats(this.currentRound(), redraw); - this.videoPlayer = this.data.videoUrls?.[0] ? new VideoPlayer(this.data.videoUrls[0], redraw) : undefined; - setInterval(() => this.redraw(true), 1000); - - const pinned = data.pinnedStream; - if (pinned && this.pinStreamer()) this.streams.push(['', pinned.name]); + if (data.videoUrls?.[0] || this.isPinnedStreamOngoing()) + this.videoPlayer = new VideoPlayer( + { + embed: this.data.videoUrls?.[0] || false, + redirect: this.data.videoUrls?.[1] || this.data.pinned?.redirect, + image: this.data.tour.image, + text: this.data.pinned?.text, + }, + redraw, + ); + const pinnedName = this.isPinnedStreamOngoing() && data.pinned?.name; + if (pinnedName) this.streams.push(['ps', pinnedName]); pubsub.on('socket.in.crowd', d => { const s = (d.streams as [string, string][]) ?? []; - if (pinned && this.pinStreamer()) s.unshift(['', pinned.name]); + if (pinnedName) s.unshift(['ps', pinnedName]); if (this.streams.length === s.length && this.streams.every(([id], i) => id === s[i][0])) return; this.streams = s; this.redraw(); }); + setInterval(() => this.redraw(true), 1000); } openTab = (t: RelayTab) => { @@ -131,10 +139,16 @@ export default class RelayCtrl { isStreamer = () => this.streams.some(([id]) => id === myUserId()); - pinStreamer = () => - defined(this.data.pinnedStream) && - !this.currentRound().finished && - Date.now() > this.currentRound().startsAt! - 1000 * 3600; + isPinnedStreamOngoing = () => { + if (!this.data.pinned) return false; + if (this.currentRound().finished) return false; + if (Date.now() < this.currentRound().startsAt! - 1000 * 3600) return false; + return true; + }; + + noEmbed() { + return document.cookie.includes('relayVideo=no'); + } private socketHandlers = { relayData: (d: RelayData) => { diff --git a/ui/analyse/src/study/relay/relayTourView.ts b/ui/analyse/src/study/relay/relayTourView.ts index d4a4a900f45df..1206492645acd 100644 --- a/ui/analyse/src/study/relay/relayTourView.ts +++ b/ui/analyse/src/study/relay/relayTourView.ts @@ -15,7 +15,6 @@ import { statsView } from './relayStats'; import { makeChatEl, type RelayViewContext } from '../../view/components'; import { gamesList } from './relayGames'; import { renderStreamerMenu } from './relayView'; -import { renderVideoPlayer } from './videoPlayer'; import { playersView } from './relayPlayers'; import { gameLinksListener } from '../studyChapters'; import { copyMeInput } from 'common/copyMe'; @@ -350,10 +349,9 @@ const teams = (ctx: RelayViewContext) => [ const stats = (ctx: RelayViewContext) => [...header(ctx), statsView(ctx.relay.stats)]; const header = (ctx: RelayViewContext) => { - const { ctrl, relay, allowVideo } = ctx; + const { ctrl, relay } = ctx; const d = relay.data, group = d.group, - embedVideo = d.videoUrls && allowVideo, studyD = ctrl.study?.data.description; return [ @@ -365,20 +363,7 @@ const header = (ctx: RelayViewContext) => { roundSelect(relay, ctx.study), ]), ]), - h( - `div.relay-tour__header__image${embedVideo ? '.video' : ''}`, - embedVideo - ? renderVideoPlayer(relay) - : d.tour.image - ? h('img', { attrs: { src: d.tour.image } }) - : ctx.study.members.isOwner() - ? h( - 'a.button.relay-tour__header__image-upload', - { attrs: { href: `/broadcast/${d.tour.id}/edit` } }, - i18n.broadcast.uploadImage, - ) - : undefined, - ), + broadcastImageOrStream(ctx), ]), studyD && h('div.relay-tour__note.pinned', h('div', [h('div', { hook: richHTML(studyD, false) })])), d.note && @@ -461,3 +446,24 @@ const roundStateIcon = (round: RelayRound, titleAsText: boolean) => { attrs: { ...dataIcon(licon.Checkmark), title: !titleAsText && i18n.site.finished } }, titleAsText && i18n.site.finished, ); + +const broadcastImageOrStream = (ctx: RelayViewContext) => { + const { relay, allowVideo } = ctx; + const d = relay.data, + embedVideo = (d.videoUrls || relay.isPinnedStreamOngoing()) && allowVideo; + + return h( + `div.relay-tour__header__image${embedVideo ? '.video' : ''}`, + embedVideo + ? relay.videoPlayer?.render() + : d.tour.image + ? h('img', { attrs: { src: d.tour.image } }) + : ctx.study.members.isOwner() + ? h( + 'a.button.relay-tour__header__image-upload', + { attrs: { href: `/broadcast/${d.tour.id}/edit` } }, + i18n.broadcast.uploadImage, + ) + : undefined, + ); +}; diff --git a/ui/analyse/src/study/relay/relayView.ts b/ui/analyse/src/study/relay/relayView.ts index e12d67c84fb40..546968987e505 100644 --- a/ui/analyse/src/study/relay/relayView.ts +++ b/ui/analyse/src/study/relay/relayView.ts @@ -6,7 +6,6 @@ import type AnalyseCtrl from '../../ctrl'; import { view as keyboardView } from '../../keyboard'; import type * as studyDeps from '../studyDeps'; import { tourSide, renderRelayTour } from './relayTourView'; -import { renderVideoPlayer } from './videoPlayer'; import { type RelayViewContext, viewContext, @@ -69,7 +68,7 @@ function renderBoardView(ctx: RelayViewContext) { return [ renderBoard(ctx), gaugeOn && cevalView.renderGauge(ctrl), - renderTools(ctx, renderVideoPlayer(ctx.relay)), + renderTools(ctx, relay.noEmbed() ? undefined : relay.videoPlayer?.render()), renderControls(ctrl), !ctrl.isEmbed && renderUnderboard(ctx), tourSide(ctx), diff --git a/ui/analyse/src/study/relay/videoPlayer.ts b/ui/analyse/src/study/relay/videoPlayer.ts index e2ce1ea4106d9..3d125e653bfe4 100644 --- a/ui/analyse/src/study/relay/videoPlayer.ts +++ b/ui/analyse/src/study/relay/videoPlayer.ts @@ -1,48 +1,32 @@ -import { looseH as h, type Redraw, type VNode } from 'common/snabbdom'; -import type RelayCtrl from './relayCtrl'; +import { looseH as h, type Redraw, type VNode, onInsert } from 'common/snabbdom'; import { allowVideo } from './relayView'; export class VideoPlayer { private iframe: HTMLIFrameElement; private close: HTMLImageElement; - private autoplay: boolean; private animationFrameId?: number; constructor( - private url: string, + private o: { embed: string | false; redirect?: string; image?: string; text?: string }, private redraw: Redraw, ) { - this.autoplay = location.search.includes('embed='); + if (!o.embed) return; this.iframe = document.createElement('iframe'); + this.iframe.setAttribute('credentialless', ''); + this.iframe.style.display = 'none'; this.iframe.id = 'video-player'; - this.iframe.setAttribute('credentialless', ''); // a feeble mewling ignored by all - if (this.autoplay) { - this.url += '&autoplay=1'; - this.iframe.allow = 'autoplay'; - } else { - this.url += '&autoplay=false'; // needs to be "false" for twitch - } - this.iframe.src = this.url; - this.iframe.setAttribute('credentialless', 'credentialless'); + this.iframe.src = o.embed; + this.iframe.allow = 'autoplay'; + this.close = document.createElement('img'); this.close.src = site.asset.flairSrc('symbols.cancel'); this.close.className = 'video-player-close'; + this.close.addEventListener('click', () => this.onEmbed('no'), true); - this.close.addEventListener('click', this.onClose, true); - - this.onWindowResize(); + this.addWindowResizer(); } - private onClose = () => { - // we need to reload the page unfortunately, - // so that a better local engine can be loaded - // once the iframe and its CSP are gone - const url = new URL(location.href); - url.searchParams.set('embed', 'no'); - window.location.replace(url); - }; - cover = (el?: HTMLElement) => { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); @@ -66,7 +50,7 @@ export class VideoPlayer { }); }; - onWindowResize = () => { + addWindowResizer = () => { let showingVideo = false; window.addEventListener( 'resize', @@ -81,16 +65,66 @@ export class VideoPlayer { { passive: true }, ); }; -} -export function renderVideoPlayer(relay: RelayCtrl): VNode | undefined { - const player = relay.videoPlayer; - return player - ? h('div#video-player-placeholder', { - hook: { - insert: (vnode: VNode) => player.cover(vnode.elm as HTMLElement), - update: (_, vnode: VNode) => player.cover(vnode.elm as HTMLElement), - }, - }) - : undefined; + render = () => { + return this.o.embed + ? h('div#video-player-placeholder', { + hook: { + insert: (vnode: VNode) => this.cover(vnode.elm as HTMLElement), + update: (_, vnode: VNode) => this.cover(vnode.elm as HTMLElement), + }, + }) + : h('div#video-player-placeholder.link', {}, [ + h('img.image', { + attrs: { src: this.o.image! }, + hook: onInsert((el: HTMLElement) => { + el.addEventListener('click', e => { + if (e.ctrlKey || e.shiftKey) window.open(this.o.redirect, '_blank'); + else this.onEmbed('ps'); + }); + el.addEventListener('contextmenu', () => window.open(this.o.redirect, '_blank')); + }), + }), + h('img.video-player-close', { + attrs: { src: site.asset.flairSrc('symbols.cancel') }, + hook: onInsert((el: HTMLElement) => el.addEventListener('click', () => this.onEmbed('no'))), + }), + this.o.text && h('div.text', h('div', this.o.text)), + h( + 'svg.play-button', + { + attrs: { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 100 100', + width: '100', + height: '100', + }, + }, + [ + h('circle', { + attrs: { + cx: '50', + cy: '50', + r: '40', + stroke: '#666', + 'stroke-width': '10', + fill: '#dddd', + }, + }), + h('path', { + attrs: { + d: 'M 32 28 A 5 5 0 0 1 37 23 L 75 45 A 6 6 0 0 1 75 55 L 37 77 A 5 5 0 0 1 32 72 Z', + fill: '#666', + }, + }), + ], + ), + ]); + }; + + onEmbed = (stream: 'ps' | 'no') => { + const urlWithEmbed = new URL(location.href); + urlWithEmbed.searchParams.set('embed', stream); + window.location.href = urlWithEmbed.toString(); + }; } From 5d39349a4f4e2eb176ae29f47a9d24ef0c0ec7d6 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 18 Dec 2024 13:15:38 -0600 Subject: [PATCH 02/18] aaarmstark changes --- public/images/icons/play-btn-youtube.svg | 21 ++++++++ ui/analyse/css/study/relay/_layout.scss | 1 + ui/analyse/css/study/relay/_tour.scss | 2 +- ui/analyse/css/study/relay/_video-player.scss | 51 ++++++++++++++----- ui/analyse/src/study/relay/videoPlayer.ts | 38 ++------------ 5 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 public/images/icons/play-btn-youtube.svg diff --git a/public/images/icons/play-btn-youtube.svg b/public/images/icons/play-btn-youtube.svg new file mode 100644 index 0000000000000..a757e4dbd0c76 --- /dev/null +++ b/public/images/icons/play-btn-youtube.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index f6c9fdc79f63b..9dc2b3f68aa5e 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -12,6 +12,7 @@ body { main.is-relay { .relay-tour { grid-area: relay-tour; + overflow: visible; &__side { grid-area: side; } diff --git a/ui/analyse/css/study/relay/_tour.scss b/ui/analyse/css/study/relay/_tour.scss index 039efc9d0162f..f134b983b6e92 100644 --- a/ui/analyse/css/study/relay/_tour.scss +++ b/ui/analyse/css/study/relay/_tour.scss @@ -117,7 +117,7 @@ $hover-bg: $m-primary_bg--mix-30; flex: 0 0 50%; } line-height: 0; - img { + > img { width: 100%; @include broken-img(2 / 1); } diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 8d8c27623bb3c..56919f8cd781c 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -30,42 +30,65 @@ #video-player-placeholder.link { cursor: pointer; + overflow: hidden; + outline-offset: -3px; + outline: 3px solid $m-bad--alpha-50; .image { - object-fit: cover; - width: 100%; - height: 100%; + position: absolute; + background: center / cover; + overflow: hidden; + inset: 0; + filter: blur(4px) brightness(0.8); } + .play-button { position: absolute; pointer-events: none; + transform: translate(-50%, -50%); top: 50%; left: 50%; - transform: translate(-50%, -50%); - opacity: 0.6; + width: 18%; + } + + &:has(.text-box) .play-button { + top: 56%; } - .text { + + .text-box { @extend %flex-column; position: absolute; + pointer-events: none; justify-content: center; align-items: center; - bottom: 5%; + top: 10%; left: 10%; right: 10%; - pointer-events: none; } - .text div { + + .text-box div { margin: auto; pointer-events: none; border-radius: 5px; border: 1px solid #888; - padding: 5px; + padding: 5px 8px; text-align: center; line-height: normal; - color: #222e; - background-color: #dddd; + color: #ddde; + background-color: #333d; + font-family: 'Noto Sans'; + font-size: 1.2em; } - &:hover:not(:has(.video-player-close:hover)) .play-button { - opacity: 1; + + &:hover:not(:has(.video-player-close:hover)) { + box-shadow: + 0 0 5px $c-bad, + 0 0 20px $c-bad; + .play-button { + filter: brightness(1.2); + } + .image { + filter: blur(4px) brightness(0.7); + } } } diff --git a/ui/analyse/src/study/relay/videoPlayer.ts b/ui/analyse/src/study/relay/videoPlayer.ts index 3d125e653bfe4..2d25d23d77b8f 100644 --- a/ui/analyse/src/study/relay/videoPlayer.ts +++ b/ui/analyse/src/study/relay/videoPlayer.ts @@ -74,9 +74,9 @@ export class VideoPlayer { update: (_, vnode: VNode) => this.cover(vnode.elm as HTMLElement), }, }) - : h('div#video-player-placeholder.link', {}, [ - h('img.image', { - attrs: { src: this.o.image! }, + : h('div#video-player-placeholder.link', [ + h('div.image', { + attrs: { style: `background-image: url(${this.o.image})` }, hook: onInsert((el: HTMLElement) => { el.addEventListener('click', e => { if (e.ctrlKey || e.shiftKey) window.open(this.o.redirect, '_blank'); @@ -89,36 +89,8 @@ export class VideoPlayer { attrs: { src: site.asset.flairSrc('symbols.cancel') }, hook: onInsert((el: HTMLElement) => el.addEventListener('click', () => this.onEmbed('no'))), }), - this.o.text && h('div.text', h('div', this.o.text)), - h( - 'svg.play-button', - { - attrs: { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 100 100', - width: '100', - height: '100', - }, - }, - [ - h('circle', { - attrs: { - cx: '50', - cy: '50', - r: '40', - stroke: '#666', - 'stroke-width': '10', - fill: '#dddd', - }, - }), - h('path', { - attrs: { - d: 'M 32 28 A 5 5 0 0 1 37 23 L 75 45 A 6 6 0 0 1 75 55 L 37 77 A 5 5 0 0 1 32 72 Z', - fill: '#666', - }, - }), - ], - ), + this.o.text && h('div.text-box', h('div', this.o.text)), + h('img.play-button', { attrs: { src: site.asset.url(`images/icons/play-btn-youtube.svg`) } }), ]); }; From eea6789d3b028781eb9203c2dbefe2e33c381960 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 18 Dec 2024 13:50:46 -0600 Subject: [PATCH 03/18] slightly less hilite on hover --- ui/analyse/css/study/relay/_video-player.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 56919f8cd781c..0bd8595bfabed 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -81,12 +81,12 @@ } &:hover:not(:has(.video-player-close:hover)) { - box-shadow: - 0 0 5px $c-bad, - 0 0 20px $c-bad; + box-shadow: 0 0 12px $c-bad; + .play-button { filter: brightness(1.2); } + .image { filter: blur(4px) brightness(0.7); } From 438241966ca61e8e26a1ccd59f43346a3f7ac24a Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 18 Dec 2024 18:51:03 -0600 Subject: [PATCH 04/18] remove youtube play button --- public/images/icons/play-btn-youtube.svg | 21 --------------- ui/analyse/css/study/relay/_video-player.scss | 13 ++++++++-- ui/analyse/src/study/relay/videoPlayer.ts | 26 ++++++++++++++++++- 3 files changed, 36 insertions(+), 24 deletions(-) delete mode 100644 public/images/icons/play-btn-youtube.svg diff --git a/public/images/icons/play-btn-youtube.svg b/public/images/icons/play-btn-youtube.svg deleted file mode 100644 index a757e4dbd0c76..0000000000000 --- a/public/images/icons/play-btn-youtube.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 0bd8595bfabed..0a63074985213 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -49,6 +49,9 @@ top: 50%; left: 50%; width: 18%; + opacity: 0.4; + stroke: white; + fill: white; } &:has(.text-box) .play-button { @@ -70,7 +73,7 @@ margin: auto; pointer-events: none; border-radius: 5px; - border: 1px solid #888; + border: 1px solid #8888; padding: 5px 8px; text-align: center; line-height: normal; @@ -84,7 +87,13 @@ box-shadow: 0 0 12px $c-bad; .play-button { - filter: brightness(1.2); + stroke: $c-bad; + fill: $c-bad; + opacity: 1; + } + + .text-box div { + border-color: $m-bad--alpha-50; } .image { diff --git a/ui/analyse/src/study/relay/videoPlayer.ts b/ui/analyse/src/study/relay/videoPlayer.ts index 2d25d23d77b8f..5a35c76e7bca4 100644 --- a/ui/analyse/src/study/relay/videoPlayer.ts +++ b/ui/analyse/src/study/relay/videoPlayer.ts @@ -90,7 +90,31 @@ export class VideoPlayer { hook: onInsert((el: HTMLElement) => el.addEventListener('click', () => this.onEmbed('no'))), }), this.o.text && h('div.text-box', h('div', this.o.text)), - h('img.play-button', { attrs: { src: site.asset.url(`images/icons/play-btn-youtube.svg`) } }), + h( + 'svg.play-button', + { + attrs: { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 100 100', + }, + }, + [ + h('circle', { + attrs: { + cx: '50', + cy: '50', + r: '45', + 'stroke-width': '10', + fill: 'transparent', + }, + }), + h('path', { + attrs: { + d: 'M 32 28 A 5 5 0 0 1 37 23 L 75 45 A 6 6 0 0 1 75 55 L 37 77 A 5 5 0 0 1 32 72 Z', + }, + }), + ], + ), ]); }; From fca9540eadcd7b73a5f131414bb7c112a9d8305a Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 20 Dec 2024 10:30:59 -0600 Subject: [PATCH 05/18] aaarmstark's masterpiece continues to evolve --- ui/analyse/css/study/relay/_video-player.scss | 7 ++++--- ui/analyse/src/study/relay/videoPlayer.ts | 12 +++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 0a63074985213..6b788997b4a07 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -50,8 +50,11 @@ left: 50%; width: 18%; opacity: 0.4; - stroke: white; fill: white; + + circle { + fill: $c-bad; + } } &:has(.text-box) .play-button { @@ -87,8 +90,6 @@ box-shadow: 0 0 12px $c-bad; .play-button { - stroke: $c-bad; - fill: $c-bad; opacity: 1; } diff --git a/ui/analyse/src/study/relay/videoPlayer.ts b/ui/analyse/src/study/relay/videoPlayer.ts index 5a35c76e7bca4..1cc685d5b379d 100644 --- a/ui/analyse/src/study/relay/videoPlayer.ts +++ b/ui/analyse/src/study/relay/videoPlayer.ts @@ -95,22 +95,20 @@ export class VideoPlayer { { attrs: { xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 100 100', + viewBox: '0 0 200 200', }, }, [ h('circle', { attrs: { - cx: '50', - cy: '50', - r: '45', - 'stroke-width': '10', - fill: 'transparent', + cx: '100', + cy: '100', + r: '90', }, }), h('path', { attrs: { - d: 'M 32 28 A 5 5 0 0 1 37 23 L 75 45 A 6 6 0 0 1 75 55 L 37 77 A 5 5 0 0 1 32 72 Z', + d: 'M 68 52 A 5 5 0 0 1 74 46 L 154 96 A 5 5 0 0 1 154 104 L 74 154 A 5 5 0 0 1 68 148 Z', }, }), ], From 3e2d3c8c5f87720668c1f3cbe334abe2935f8739 Mon Sep 17 00:00:00 2001 From: John Doknjas Date: Sat, 21 Dec 2024 07:38:54 +0000 Subject: [PATCH 06/18] On other platforms (e.g., ios), prevent the user from holding down and selecting in certain areas of the homepage. --- ui/common/css/abstract/_mixins.scss | 7 +++++++ ui/common/css/header/_header.scss | 2 +- ui/lobby/css/_table.scss | 1 + ui/lobby/css/app/_app.scss | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/common/css/abstract/_mixins.scss b/ui/common/css/abstract/_mixins.scss index 53c629d980f19..cae7c94feb00e 100644 --- a/ui/common/css/abstract/_mixins.scss +++ b/ui/common/css/abstract/_mixins.scss @@ -156,3 +156,10 @@ overflow: hidden; text-overflow: ellipsis; } + +@mixin prevent-select { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} \ No newline at end of file diff --git a/ui/common/css/header/_header.scss b/ui/common/css/header/_header.scss index a246b28ed6a96..a44b898435dd9 100644 --- a/ui/common/css/header/_header.scss +++ b/ui/common/css/header/_header.scss @@ -11,7 +11,7 @@ body > header { z-index: $z-site-header-106; max-width: 1800px; margin: 0 auto; - user-select: none; + @include prevent-select; @include mq-sticky-header { max-width: unset; diff --git a/ui/lobby/css/_table.scss b/ui/lobby/css/_table.scss index 00222a9a36230..45d80baee2ea3 100644 --- a/ui/lobby/css/_table.scss +++ b/ui/lobby/css/_table.scss @@ -1,6 +1,7 @@ .lobby { &__table { @extend %flex-column; + @include prevent-select; position: relative; } diff --git a/ui/lobby/css/app/_app.scss b/ui/lobby/css/app/_app.scss index 2fe0bb360b589..024a5b271f4cd 100644 --- a/ui/lobby/css/app/_app.scss +++ b/ui/lobby/css/app/_app.scss @@ -7,6 +7,7 @@ @extend %flex-column; @include lobby-app-size; + @include prevent-select; user-select: none; From 2652edd793d52547017d265963b2898df9ffc794 Mon Sep 17 00:00:00 2001 From: John Doknjas Date: Sat, 21 Dec 2024 07:57:57 +0000 Subject: [PATCH 07/18] Use `prevent-select` to replace some existing css properties. --- ui/analyse/css/_context-menu.scss | 3 +-- ui/common/css/component/_chart.scss | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/analyse/css/_context-menu.scss b/ui/analyse/css/_context-menu.scss index 93ba9a97d95ab..e055c7663b9df 100644 --- a/ui/analyse/css/_context-menu.scss +++ b/ui/analyse/css/_context-menu.scss @@ -1,13 +1,12 @@ #analyse-cm { @extend %box-radius, %popup-shadow; + @include prevent-select; background: $c-bg-box; position: absolute; display: none; z-index: $z-context-menu-108; cursor: default; - user-select: none; - -webkit-user-select: none; &.visible { display: block; diff --git a/ui/common/css/component/_chart.scss b/ui/common/css/component/_chart.scss index a5a5642394f26..5d68c47ffc2dc 100644 --- a/ui/common/css/component/_chart.scss +++ b/ui/common/css/component/_chart.scss @@ -37,14 +37,11 @@ */ .noUi-target, .noUi-target * { + @include prevent-select; -webkit-touch-callout: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-user-select: none; -ms-touch-action: none; touch-action: none; - -ms-user-select: none; - -moz-user-select: none; - user-select: none; -moz-box-sizing: border-box; box-sizing: border-box; } From dca6c4c840928a19bf204bf0309b206438b265a5 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sat, 21 Dec 2024 00:07:26 -0800 Subject: [PATCH 08/18] Format with prettier. --- ui/common/css/abstract/_mixins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/common/css/abstract/_mixins.scss b/ui/common/css/abstract/_mixins.scss index cae7c94feb00e..d8b5e5ed06564 100644 --- a/ui/common/css/abstract/_mixins.scss +++ b/ui/common/css/abstract/_mixins.scss @@ -162,4 +162,4 @@ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -} \ No newline at end of file +} From cfa1cfd492ddebff67eec551ce0e44a63fcd9cc2 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 21 Dec 2024 23:26:38 +0100 Subject: [PATCH 09/18] relay http refactors --- modules/relay/src/main/RelayFetch.scala | 99 +++++++++++------------- modules/relay/src/main/RelayFormat.scala | 62 +++++++-------- 2 files changed, 75 insertions(+), 86 deletions(-) diff --git a/modules/relay/src/main/RelayFetch.scala b/modules/relay/src/main/RelayFetch.scala index 620e910937ac8..c20682af57baa 100644 --- a/modules/relay/src/main/RelayFetch.scala +++ b/modules/relay/src/main/RelayFetch.scala @@ -301,33 +301,28 @@ final private class RelayFetch( .map { MultiPgn.split(_, RelayFetch.maxGamesToRead(rt.tour.official)) } .flatMap(multiPgnToGames.future) case RelayFormat.LccWithGames(lcc) => - lccRoundJsonWithEtag - .fetch(lcc.indexUrl) - .flatMap: round => - val lookForStart: Boolean = - rt.round.startsAtTime - .map(_.minusSeconds(rt.round.sync.delay.so(_.value) + 5 * 60)) - .forall(_.isBeforeNow) - round.pairings - .mapWithIndex: (pairing, i) => - val game = i + 1 - val tags = pairing.tags(lcc.round, game, round.date) - lccCache(lcc, game, tags, lookForStart): () => - lccGameJsonWithEtag - .fetch(lcc.gameUrl(game)) - .recover: - case _: Exception => GameJson(moves = Nil, result = none) - .map { _.toPgn(tags) } - .recover: _ => - PgnStr(s"${tags}\n\n${pairing.result}") - .map(game -> _) - .parallel - .map: pgns => - MultiPgn(pgns.sortBy(_._1).map(_._2)) - .flatMap(multiPgnToGames.future) + lccRoundJsonWithEtag(lcc.indexUrl).flatMap: round => + val lookForStart: Boolean = + rt.round.startsAtTime + .map(_.minusSeconds(rt.round.sync.delay.so(_.value) + 5 * 60)) + .forall(_.isBeforeNow) + round.pairings + .mapWithIndex: (pairing, i) => + val game = i + 1 + val tags = pairing.tags(lcc.round, game, round.date) + lccCache(lcc, game, tags, lookForStart): () => + lccGameJsonWithEtag(lcc.gameUrl(game)).recover: + case _: Exception => GameJson(moves = Nil, result = none) + .map { _.toPgn(tags) } + .recover: _ => + PgnStr(s"${tags}\n\n${pairing.result}") + .map(game -> _) + .parallel + .map: pgns => + MultiPgn(pgns.sortBy(_._1).map(_._2)) + .flatMap(multiPgnToGames.future) case RelayFormat.LccWithoutGames(lcc) => - lccRoundJsonWithEtag - .fetch(lcc.indexUrl) + lccRoundJsonWithEtag(lcc.indexUrl) .map: round => MultiPgn: round.pairings.mapWithIndex: (pairing, i) => @@ -343,34 +338,34 @@ final private class RelayFetch( data <- summon[Reads[A]].reads(json).fold(err => fufail(s"Invalid JSON from $url: $err"), fuccess) yield data - private final class JsonWithEtag[A: Reads](initialCapacity: Int): + // lcc supports https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match + private def fetchJsonWithEtag[A: Reads](initialCapacity: Int): URL => CanProxy ?=> Fu[A] = import RelayFormat.Etag - // lcc uses https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match - private val cache = cacheApi.notLoadingSync[URL, (Etag, A)](initialCapacity, "relay.fetch.jsonWithEtag"): + val cache = cacheApi.notLoadingSync[URL, (Etag, A)](initialCapacity, "relay.fetch.jsonWithEtag"): _.expireAfterWrite(5 minutes).build() - - def fetch(url: URL)(using CanProxy): Fu[A] = - cache - .getIfPresent(url) - .match - case None => - for - (body, newEtag) <- formatApi.httpGetWithEtag(url, none) - data <- readAsJson[A](url)(~body) - yield (data, newEtag) - case Some((etag, prev)) => - for - (body, newEtag) <- formatApi.httpGetWithEtag(url, etag.some) - isHit = body.isEmpty && newEtag.contains(etag) - _ = lila.mon.relay.etag(isHit).increment() - data <- if isHit then fuccess(prev) else readAsJson[A](url)(~body) - yield (data, newEtag) - .map: (data, newEtag) => - newEtag.foreach(e => cache.put(url, e -> data)) - data - - private val lccGameJsonWithEtag = new JsonWithEtag[DgtJson.GameJson](512) - private val lccRoundJsonWithEtag = new JsonWithEtag[DgtJson.RoundJson](32) + url => + CanProxy ?=> + cache + .getIfPresent(url) + .match + case None => + for + (body, newEtag) <- formatApi.httpGetWithEtag(url, none) + data <- readAsJson[A](url)(~body) + yield (data, newEtag) + case Some((etag, prev)) => + for + (body, newEtag) <- formatApi.httpGetWithEtag(url, etag.some) + isHit = body.isEmpty && newEtag.forall(_ == etag) // on 304 response, Etag might be empty + _ = lila.mon.relay.etag(isHit).increment() + data <- if isHit then fuccess(prev) else readAsJson[A](url)(~body) + yield (data, newEtag.orElse(etag.some)) + .map: (data, newEtag) => + newEtag.foreach(e => cache.put(url, e -> data)) + data + + private val lccGameJsonWithEtag = fetchJsonWithEtag[DgtJson.GameJson](512) + private val lccRoundJsonWithEtag = fetchJsonWithEtag[DgtJson.RoundJson](32) private object RelayFetch: diff --git a/modules/relay/src/main/RelayFormat.scala b/modules/relay/src/main/RelayFormat.scala index f32a5f983564b..3031d7c358d9f 100644 --- a/modules/relay/src/main/RelayFormat.scala +++ b/modules/relay/src/main/RelayFormat.scala @@ -10,7 +10,6 @@ import play.api.libs.ws.{ StandaloneWSRequest, StandaloneWSResponse } - import scala.util.matching.Regex import lila.core.config.{ Credentials, HostPort } @@ -70,54 +69,50 @@ final private class RelayFormatApi( roundRepo.exists(id).map(_.option(RelayFormat.Round(id))) def httpGet(url: URL)(using CanProxy): Fu[String] = - httpGetResponse(url).map(_.body) + httpGetResponse(toRequest(url)).map(_.body) def httpGetAndGuessCharset(url: URL)(using CanProxy): Fu[String] = - httpGetResponse(url).map: res => + httpGetResponse(toRequest(url)).map: res => responseHeaderCharset(res) match case None => lila.common.String.charset.guessAndDecode(res.bodyAsBytes) case Some(known) => res.bodyAsBytes.decodeString(known) - def httpGetWithEtag(url: URL, etag: Option[Etag])(using - CanProxy - ): Fu[(Option[String], Option[Etag])] = - val (req, proxy) = configure(url) - etag - .fold(req)(etag => req.addHttpHeaders("If-None-Match" -> etag)) - .get() + def httpGetWithEtag(url: URL, etag: Option[Etag])(using CanProxy): Fu[(Option[String], Option[Etag])] = + val req = etag + .foldLeft(toRequest(url))((req, etag) => req.addHttpHeaders("If-None-Match" -> etag)) + httpGetResponse(req) .flatMap: res => val newEtag = res.header("Etag") if res.status == 304 then fuccess(none -> newEtag.orElse(etag)) - else if res.status == 200 then fuccess((res.body: String).some -> newEtag) - else if res.status == 404 then fufail(NotFound(url)) - else fufail(s"[${res.status}] $url") - .monSuccess(_.relay.httpGet(url.host.toString, proxy)) - - private def httpGetResponse(url: URL)(using CanProxy): Future[StandaloneWSResponse] = - val (req, proxy) = configure(url) - req - .get() - .flatMap: res => - if res.status == 200 then fuccess(res) - else if res.status == 404 then fufail(NotFound(url)) - else fufail(s"[${res.status}] $url") - .monSuccess(_.relay.httpGet(url.host.toString, proxy)) + else fuccess((res.body: String).some -> newEtag) + .monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host))) + + private def httpGetResponse(req: StandaloneWSRequest)(using CanProxy): Future[StandaloneWSResponse] = + Future + .fromTry(lila.common.url.parse(req.url)) + .flatMap: url => + req + .get() + .flatMap: res => + if res.status == 200 || res.status == 304 then fuccess(res) + else if res.status == 404 then fufail(NotFound(url)) + else fufail(s"[${res.status}] ${req.url}") + .monSuccess(_.relay.httpGet(url.host.toString, req.proxyServer.map(_.host))) private def responseHeaderCharset(res: StandaloneWSResponse): Option[java.nio.charset.Charset] = import play.shaded.ahc.org.asynchttpclient.util.HttpUtils Option(HttpUtils.extractContentTypeCharsetAttribute(res.contentType)).orElse: res.contentType.startsWith("text/").option(java.nio.charset.StandardCharsets.ISO_8859_1) - private def configure(url: URL)(using CanProxy): (StandaloneWSRequest, Option[String]) = - addProxy(url): - ws.url(url.toString) - .withRequestTimeout(5.seconds) - .withFollowRedirects(false) + private def toRequest(url: URL)(using CanProxy): StandaloneWSRequest = + val req = ws + .url(url.toString) + .withRequestTimeout(5.seconds) + .withFollowRedirects(false) + proxyServerFor(url).foldLeft(req)(_ withProxyServer _) - private def addProxy(url: URL)(ws: StandaloneWSRequest)(using - allowed: CanProxy - ): (StandaloneWSRequest, Option[String]) = - def server = for + private def proxyServerFor(url: URL)(using allowed: CanProxy): Option[DefaultWSProxyServer] = + for hostPort <- proxyHostPort.get() if allowed.yes proxyRegex = proxyDomainRegex.get() @@ -130,7 +125,6 @@ final private class RelayFormatApi( principal = creds.map(_.user), password = creds.map(_.password.value) ) - server.foldLeft(ws)(_ withProxyServer _) -> server.map(_.host) private def looksLikePgn(body: String)(using CanProxy): Boolean = MultiPgn From 00eca617cf8068d6d2da59aeca959050d88e05aa Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Sat, 21 Dec 2024 16:54:36 -0600 Subject: [PATCH 10/18] hopefully final css tweaks --- ui/analyse/css/study/relay/_video-player.scss | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ui/analyse/css/study/relay/_video-player.scss b/ui/analyse/css/study/relay/_video-player.scss index 6b788997b4a07..6990acf1968d4 100644 --- a/ui/analyse/css/study/relay/_video-player.scss +++ b/ui/analyse/css/study/relay/_video-player.scss @@ -39,7 +39,7 @@ background: center / cover; overflow: hidden; inset: 0; - filter: blur(4px) brightness(0.8); + filter: blur(4px) brightness(0.7); } .play-button { @@ -49,10 +49,15 @@ top: 50%; left: 50%; width: 18%; - opacity: 0.4; + opacity: 0.6; fill: white; + filter: drop-shadow(0 0 12px #0000004f); circle { + filter: drop-shadow(0 0 8px #840000); + paint-order: stroke fill; + stroke: #fff9; + stroke-width: 3px; fill: $c-bad; } } @@ -93,12 +98,8 @@ opacity: 1; } - .text-box div { - border-color: $m-bad--alpha-50; - } - .image { - filter: blur(4px) brightness(0.7); + filter: blur(4px) brightness(0.6); } } } From 644a5fbd652804cc616f58e0c29d140c0e9d67c6 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sat, 21 Dec 2024 18:44:03 -0800 Subject: [PATCH 11/18] Remove obsolete vendor prefixes. For lichess' supported browser versions, Safari is the only one that needs it. --- ui/common/css/abstract/_mixins.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/common/css/abstract/_mixins.scss b/ui/common/css/abstract/_mixins.scss index d8b5e5ed06564..4f8de671e4938 100644 --- a/ui/common/css/abstract/_mixins.scss +++ b/ui/common/css/abstract/_mixins.scss @@ -160,6 +160,4 @@ @mixin prevent-select { user-select: none; -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; } From ad023f1db175a4acc75b4d0b1cb05aad64244d25 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:00:32 -0600 Subject: [PATCH 12/18] Update README.md --- ui/common/css/theme/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/common/css/theme/README.md b/ui/common/css/theme/README.md index 32a0f6ce966ad..02864bc462d4b 100644 --- a/ui/common/css/theme/README.md +++ b/ui/common/css/theme/README.md @@ -117,7 +117,7 @@ from the $c-font and $c-bg colors you defined in the theme file. same as before ``` -ui/build -r +ui/build -w ``` watch mode should keep everything in sync for you, but you might need to restart if From afd3e134077c787a96ccd0614667e5149d033508 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:40:50 -0600 Subject: [PATCH 13/18] Update README.md --- ui/common/css/theme/README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/ui/common/css/theme/README.md b/ui/common/css/theme/README.md index 02864bc462d4b..f23ea02ac3167 100644 --- a/ui/common/css/theme/README.md +++ b/ui/common/css/theme/README.md @@ -1,13 +1,4 @@ -# IMPORTANT - -### If you're just seeing this for the first time, you probably want to run: - -``` -ui/build --update --clean -``` - -`--update` tells ui/build, which has recently acquired new capabilities, to update itself. -`--clean` tells it to clean up the mess it made last time. +### notes on color themes ## css variables vs scss variables From 17f3d89f1e1efc1d635c66322ea2807b68f7bea6 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:41:15 -0600 Subject: [PATCH 14/18] last one --- ui/common/css/theme/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/common/css/theme/README.md b/ui/common/css/theme/README.md index f23ea02ac3167..f040e5797f659 100644 --- a/ui/common/css/theme/README.md +++ b/ui/common/css/theme/README.md @@ -1,5 +1,3 @@ -### notes on color themes - ## css variables vs scss variables - scss variables start with $ (dollar sign) and are compile-time macros. when the css is From ba0548b0215e0737d81809a5bc844c21879bc861 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 22 Dec 2024 08:10:44 +0100 Subject: [PATCH 15/18] New Crowdin updates (#16644) * New translations: class.xml (Russian) * New translations: streamer.xml (Russian) * New translations: preferences.xml (Russian) * New translations: streamer.xml (Ukrainian) * New translations: class.xml (Portuguese) * New translations: streamer.xml (Portuguese) * New translations: preferences.xml (Portuguese) * New translations: activity.xml (Chinese Simplified) * New translations: broadcast.xml (Chinese Simplified) * New translations: streamer.xml (Vietnamese) * New translations: preferences.xml (Korean) * New translations: class.xml (Korean) * New translations: streamer.xml (Korean) * New translations: timeago.xml (Marathi) * New translations: site.xml (Tamil) * New translations: activity.xml (Tamil) * New translations: class.xml (Tamil) * New translations: preferences.xml (Tamil) * New translations: study.xml (Chinese Traditional) * New translations: class.xml (Chinese Traditional) * New translations: streamer.xml (Chinese Traditional) * New translations: preferences.xml (Chinese Traditional) * New translations: team.xml (Chinese Traditional) * New translations: swiss.xml (Chinese Traditional) --- translation/dest/activity/ta-IN.xml | 4 + translation/dest/activity/zh-CN.xml | 3 + translation/dest/broadcast/zh-CN.xml | 3 + translation/dest/class/ko-KR.xml | 6 +- translation/dest/class/pt-PT.xml | 6 +- translation/dest/class/ru-RU.xml | 6 +- translation/dest/class/ta-IN.xml | 2 +- translation/dest/class/zh-TW.xml | 6 +- translation/dest/preferences/ko-KR.xml | 5 +- translation/dest/preferences/pt-PT.xml | 5 +- translation/dest/preferences/ru-RU.xml | 5 +- translation/dest/preferences/ta-IN.xml | 4 +- translation/dest/preferences/zh-TW.xml | 5 +- translation/dest/site/ta-IN.xml | 116 ++++++++++++------------- translation/dest/streamer/ko-KR.xml | 5 ++ translation/dest/streamer/pt-PT.xml | 6 +- translation/dest/streamer/ru-RU.xml | 5 ++ translation/dest/streamer/uk-UA.xml | 2 + translation/dest/streamer/vi-VN.xml | 5 ++ translation/dest/streamer/zh-TW.xml | 5 ++ translation/dest/study/zh-TW.xml | 1 + translation/dest/swiss/zh-TW.xml | 6 ++ translation/dest/team/zh-TW.xml | 1 + translation/dest/timeago/mr-IN.xml | 20 ++--- 24 files changed, 148 insertions(+), 84 deletions(-) diff --git a/translation/dest/activity/ta-IN.xml b/translation/dest/activity/ta-IN.xml index 5c214954e62cd..93fac056f1b59 100644 --- a/translation/dest/activity/ta-IN.xml +++ b/translation/dest/activity/ta-IN.xml @@ -42,6 +42,10 @@ %s பிளேயரைப் பின்தொடரத் தொடங்கியது %s வீரர்களைப் பின்தொடரத் தொடங்கியது + + %ஒரு புதிய பின்தொடர்பவர் கிடைத்தது + %s புதிய பின்தொடர்பவர்களைப் பெற்றார் + %s ஒரே நேரத்தில் கண்காட்சி நடத்தப்பட்டது %s ஒரே நேரத்தில் கண்காட்சிகள் நடத்தப்பட்டன diff --git a/translation/dest/activity/zh-CN.xml b/translation/dest/activity/zh-CN.xml index 7f8e6682b42d6..2a1a4c1ddf8b1 100644 --- a/translation/dest/activity/zh-CN.xml +++ b/translation/dest/activity/zh-CN.xml @@ -26,6 +26,9 @@ 完成了 %s 盘通讯棋 + + 下完了%1$s%2$s局通信棋局 + 新关注 %s 个用户 diff --git a/translation/dest/broadcast/zh-CN.xml b/translation/dest/broadcast/zh-CN.xml index bac593f9ce979..665d425e9828a 100644 --- a/translation/dest/broadcast/zh-CN.xml +++ b/translation/dest/broadcast/zh-CN.xml @@ -47,6 +47,9 @@ 今年的年龄 未评级 最近的比赛 + 团队 + 官网 + 官方排名 结束的转播 按月查看所有转播 diff --git a/translation/dest/class/ko-KR.xml b/translation/dest/class/ko-KR.xml index 92ac38c54e6a3..cb116f3ca01ed 100644 --- a/translation/dest/class/ko-KR.xml +++ b/translation/dest/class/ko-KR.xml @@ -6,7 +6,7 @@ 학생을 위한 안전한 사용자 이름과 비밀번호를 빠르게 생성합니다 게임과 퍼즐에서 학생의 진도를 추적합니다 새로운 수업 자료에 대해 모든 학생들에게 메시지를 보냅니다 - 광고나 추적기 없이 모두에게, 영원히, 100% 무료 + 광고나 추적기 없이 모두에게, 영원히, 100% 무료 Lichess 교사 지원하기 수업이 없습니다. 교사: %s @@ -56,6 +56,8 @@ %s개의 대기중인 초대 + 보류 중 + 거절한다 클래스 선생님에게만 보입니다 활성화 관리 @@ -111,4 +113,6 @@ %s에게 초대장을 전송하였습니다 %s은(는) 이미 초대되었습니다 %1$s는 어린이 계정이므로 메시지를 받을 수 없습니다. 초대 링크를 수동으로 보내야 합니다: %2$s. + %s로 움직여 + 다른 수업으로 움직여 diff --git a/translation/dest/class/pt-PT.xml b/translation/dest/class/pt-PT.xml index 2f44d94833fa7..34c79988c1b68 100644 --- a/translation/dest/class/pt-PT.xml +++ b/translation/dest/class/pt-PT.xml @@ -6,7 +6,7 @@ Gera rapidamente nomes de utilizadores e palavras-passe seguros para os alunos Acompanha o progresso do aluno em jogos e em problemas Envia uma mensagem a todos os alunos sobre o novo material didático - 100% gratuito para todos, para sempre, sem anúncios ou rastreadores + 100% gratuito para todos, para sempre, sem anúncios ou rastreadores Candidata-te para seres um professor Lichess Não há aulas ainda. Professores: %s @@ -57,6 +57,8 @@ Aqui está o link para acederes à aula. Um convite pendente %s convites pendentes + Pendente + Recusado Visível apenas aos professores da turma Ativo Gerido @@ -113,4 +115,6 @@ Aqui está o link para acederes à aula. Foi enviado um convite para %s %s já tem um convite pendente %1$s é uma conta de criança e não pode receber a sua mensagem. Deve dar-lhe o URL de convite manualmente: %2$s + Mover para%s + Mover para outra aula diff --git a/translation/dest/class/ru-RU.xml b/translation/dest/class/ru-RU.xml index 36bdc98018c78..e4bd72e2033ef 100644 --- a/translation/dest/class/ru-RU.xml +++ b/translation/dest/class/ru-RU.xml @@ -6,7 +6,7 @@ Быстро создавайте безопасные имена и пароли для учеников Отслеживайте прогресс учеников в партиях и задачах Сообщайте всем ученикам о новых материалах класса - Бесплатно для всех, навсегда, без рекламы и отслеживания на 100 % + Бесплатно для всех, навсегда, без рекламы и отслеживания на 100 % Подайте заявку, чтобы стать Преподавателем Lichess Ещё нет классов. Преподаватели: %s @@ -59,6 +59,8 @@ %s приглашений, ожидающих рассмотрения %s приглашений, ожидающих рассмотрения + В ожидании + Отклонено Доступны для просмотра только преподавателю Активный Управляемый @@ -119,4 +121,6 @@ Приглашение пользователю %s было отправлено Вы уже отправили приглашение %s %1$s детский аккаунт и не может получить ваше сообщение. Вам нужно передать им ссылку на приглашение вручную: %2$s + Перейти к %s + Переместить в другой класс diff --git a/translation/dest/class/ta-IN.xml b/translation/dest/class/ta-IN.xml index 7c1a6d1dc3b6b..445d5d9a49e48 100644 --- a/translation/dest/class/ta-IN.xml +++ b/translation/dest/class/ta-IN.xml @@ -6,7 +6,7 @@ மாணவர்களுக்கான பாதுகாப்பான பயனர்பெயர்கள் மற்றும் கடவுச்சொற்களை விரைவாக உருவாக்கவும் விளையாட்டுகள் மற்றும் புதிர்களில் மாணவர் முன்னேற்றத்தைக் கண்காணிக்கவும் வகுப்பில் புதிதாகச் சேர்க்கப்பட்டதை அனைத்து மாணவர்களுக்கும் அறிவிக்கவும் - அனைவருக்கும், என்றென்றும் 100% சுதந்திரமாக, விளம்பரமோ பின்தொடர்தல்களோ இல்லாமல் + அனைவருக்கும், என்றென்றும் 100% சுதந்திரமாக, விளம்பரமோ பின்தொடர்தல்களோ இல்லாமல் லிசெஸ் ஆசிரியராக விண்ணப்பியுங்கள் இன்னும் வகுப்புகள் இல்லை. ஆசிரியர்: %s diff --git a/translation/dest/class/zh-TW.xml b/translation/dest/class/zh-TW.xml index db621b9c5061a..35b7bd8ff7a80 100644 --- a/translation/dest/class/zh-TW.xml +++ b/translation/dest/class/zh-TW.xml @@ -6,7 +6,7 @@ 快速為學生建立安全的使用者名稱及密碼 紀錄學生在棋局和謎題中的進度 通知所有學生關於新教材 - 完全免費,一視同仁。且無廣告及追蹤器 + 完全免費,一視同仁。且無廣告及追蹤器 申請成為 Lichess 的教師 還沒有課程 %s個教師 @@ -56,6 +56,8 @@ %s個待處理的邀請 + 等待中 + 已拒絕 僅對班級老師可見 活動 管理 @@ -111,4 +113,6 @@ 邀請已發送到 %s %s 已經有待處理的邀請 %1$s 是未成年帳戶,無法接收您的消息。您必須手動給他發送邀請鏈接: %2$s + 移動到 %s + 移動到另一個課程 diff --git a/translation/dest/preferences/ko-KR.xml b/translation/dest/preferences/ko-KR.xml index 1fd8d79007aac..6de3f4929dcbc 100644 --- a/translation/dest/preferences/ko-KR.xml +++ b/translation/dest/preferences/ko-KR.xml @@ -20,9 +20,10 @@ 보드 크기 재조정 핸들 보이기 초기 상태에서만 게임 도중에만 적용 + 게임 내 제외 체스 시계 1/10초 단위 - 남은 시간이 10초 미만일 때 + 남은 시간이 10초 미만일 때 녹색 수평 진행 바 시간이 얼마 안 남았을 때 소리 재생 시간 더 주기 @@ -38,7 +39,7 @@ 일시적으로 자동 승진을 끄기 위해 승진하는 동안 <ctrl>를 누르세요 미리두기 때만 3회 동형반복시 자동으로 무승부 요청 - 남은 시간이 30초 미만일 때만 + 남은 시간이 30초 미만일 때만 수 확인 경기 도중 보드 메뉴에서 비활성화될 수 있습니다. 통신 대국 diff --git a/translation/dest/preferences/pt-PT.xml b/translation/dest/preferences/pt-PT.xml index 48e2b1b699e8d..f96f2aa4e1d8d 100644 --- a/translation/dest/preferences/pt-PT.xml +++ b/translation/dest/preferences/pt-PT.xml @@ -20,9 +20,10 @@ Mostrar o cursor de redimensionamento do tabuleiro Apenas na posição inicial Apenas em Jogo + Exceto no jogo Relógio de xadrez Décimos de segundo - Quando o tempo restante for < 10 segundos + Quando o tempo restante for < 10 segundos Barras de progresso verdes horizontais Som ao atingir tempo crítico Dar mais tempo @@ -38,7 +39,7 @@ Mantenha a tecla <ctrl> pressionada enquanto promove para desativar temporariamente a autopromoção Quando mover antecipadamente Reivindicar empate automaticamente após uma repetição tripla - Quando o tempo restante for < 30 segundos + Quando o tempo restante for < 30 segundos Confirmação de movimento Pode ser desativado durante um jogo com o menu do tabuleiro Jogos por correspondência diff --git a/translation/dest/preferences/ru-RU.xml b/translation/dest/preferences/ru-RU.xml index e3c207b5ce3bd..d677374116541 100644 --- a/translation/dest/preferences/ru-RU.xml +++ b/translation/dest/preferences/ru-RU.xml @@ -20,9 +20,10 @@ Показывать ручку изменения размера доски Только в начальном положении Только в игре + Кроме как во время игры Шахматные часы Десятые доли секунд - Когда остаётся меньше 10 секунд + Когда остаётся меньше 10 секунд Убывающий зелёный индикатор Звук, когда время подходит к концу Добавить времени @@ -38,7 +39,7 @@ Удерживайте клавишу <ctrl> во время превращения, чтобы временно отключить автопревращение в ферзя Когда сделан предварительный ход Автоматически запрашивать ничью при трёхкратном повторении хода - Когда остаётся меньше 30 секунд + Когда остаётся меньше 30 секунд Подтверждение хода Может быть отключено во время игры вместе с меню доски В игре по переписке diff --git a/translation/dest/preferences/ta-IN.xml b/translation/dest/preferences/ta-IN.xml index a771a8482bdb8..95b226716d710 100644 --- a/translation/dest/preferences/ta-IN.xml +++ b/translation/dest/preferences/ta-IN.xml @@ -22,7 +22,7 @@ விளையாட்டில் மட்டும் செஸ் கடிகாரம் பத்தில் ஒரு பங்கு வினாடிகள் - மீதமுள்ள நேரம் < 10 வினாடிகள் + மீதமுள்ள நேரம் < 10 வினாடிகள் கிடைமட்ட பச்சை முன்னேற்றப் பட்டைகள் நேரம் முக்கியமானதாக இருக்கும்போது ஒலி கூடுதல் நேரம் வழங்கு @@ -38,7 +38,7 @@ <ctrl> தானியங்கு விளம்பரத்தை தற்காலிகமாக முடக்குவதற்கு விளம்பரப்படுத்தும் போது முக்கியமானது முன்செல்லும் போது தானாக மூன்று மடங்கு திரும்ப திரும்ப உரிமை கோரவும் - மீதமுள்ள நேரம் < 30 வினாடிகள் + மீதமுள்ள நேரம் < 30 வினாடிகள் நகர்த்த உறுதிப்படுத்தல் போர்டு மெனுவுடன் விளையாட்டின் போது முடக்கப்படலாம் கடித விளையாட்டுகள் diff --git a/translation/dest/preferences/zh-TW.xml b/translation/dest/preferences/zh-TW.xml index df3eb3cbd371a..9107a3908f356 100644 --- a/translation/dest/preferences/zh-TW.xml +++ b/translation/dest/preferences/zh-TW.xml @@ -20,9 +20,10 @@ 顯示盤面大小調整區塊 只在起始局面 只在遊戲中 + 僅適用於非評分局中 棋鐘 十分之一秒 - 當剩餘時間小於10秒 + 當剩餘時間小於10秒 綠色橫進度條 時間不足時聲音提醒 給對方更多時間 @@ -38,7 +39,7 @@ 升變的同時按住<ctrl>以暫時取消自動升變 預先走棋時 在三次重覆局面時自動要求和局 - 當剩餘時間小於30秒 + 當剩餘時間小於30秒 走棋確認 可以在遊戲中用棋盤選單中關閉此功能 在長期對局中 diff --git a/translation/dest/site/ta-IN.xml b/translation/dest/site/ta-IN.xml index 5334dbcdd965d..da8a69e4b8b14 100644 --- a/translation/dest/site/ta-IN.xml +++ b/translation/dest/site/ta-IN.xml @@ -1,62 +1,62 @@ - ஒரு கேளிருடன் விளையாடு - கணிப்பொறியோடு விளையாடு - விளையாட்டுக்கு அழைக்க இந்த URL ளை அனுப்பு + கேளிருடன் விளையாடு + கணினியுடன் விளையாடு + பிறரை ஆட்டத்திற்கு அழைக்க, இந்த URL ளை கொடு ஆட்டம் முடிந்தது எதிராளிக்காகக் காத்திருக்கின்றது - அல்லது உங்கள் எதிரி இந்த QR குறியீட்டை ஸ்கேன் செய்ய அனுமதிக்கவும் + அல்லது உங்கள் எதிராளியை இந்த QR குறியீட்டை மேவ அனுமதிக்கவும் காத்திருக்கின்றது தங்கள் முறை %1$s நிலைமட்டம் %2$s நிலைமட்டம் வலிமை - உரையாடலை மாற்று + உரையாலை மாற்று உரையாடு விலகு - நிகர்தடை - நகர்த்த வாய்ப்பின்மை + முற்றுகை + இக்கட்டு வெள்ளை கருப்பு - வெள்ளைக் காய்களுடன் + வெள்ளையாக கறுப்பாக - ஏதாவது ஒரு நிறம் - ஆட்டமொன்றை உருவாக்கு + ஏதோவொரு புறம் + ஆட்டமொன்றை ஆக்கு வெள்ளை வென்றது கருப்பு வென்றது - நீங்கள் வெள்ளை நிறத்திலுள்ள காய்களுடன் ஆடுகிறீர்கள் - நீங்கள் கருப்பு நிறத்திலுள்ள காய்களுடன் ஆடுகிறீர்கள் - நீங்கள் விளையாடனும்! - மோசடி கண்டுபிடிக்கப்பட்டது + நீங்கள் வெள்ளை நிற உருப்படிகளுடன் ஆடுகிறீர்கள் + நீங்கள் கருப்பு நிற உருப்படிகளுடன் ஆடுகிறீர்கள் + உங்கள் முறை! + வஞ்சித்தல் கண்டறியப்பட்டது மையத்தில் அரசன் மூன்று இடையூறுகள் - ஓட்டம் முடிந்ததும் - மாற்று முடிவுக்கு + ஓட்டம் முற்றிற்று + திரிபுரு முடிவடைகிறது புது எதிராளி நும் எதிராளி நும்மோடு புது ஆட்டமாட விரும்புகிறார் - ஆட்டத்தில் சேரவும் - வெள்ளை விளையாடுகின்றது - கருப்பு விளையாடுகின்றது + ஆட்டத்தில் பொருந்தவும் + வெள்ளை விளையாட + கருப்பு விளையாட - உங்கள் எதிரி ஆட்டத்தை விட்டு அகன்றுள்ளார். இன்னும் %s வினாடியில் நீங்கள் வெற்றியை கோரலாம். - உங்கள் எதிரி ஆட்டத்தை விட்டு அகன்றுள்ளார். இன்னும் %s வினாடிகளில் நீங்கள் வெற்றியை கோரலாம். + உங்கள் எதிராளி ஆட்டத்தை விட்டு அகன்றுள்ளார். இன்னும் %s நொடியில் நீங்கள் வெற்றியை உரிமைகோரலாம். + உங்கள் எதிராளி ஆட்டத்தை விட்டு அகன்றுள்ளார். இன்னும் %s நொடிகளில் நீங்கள் வெற்றியை உரிமைகோரலாம். - எதிராளி ஆட்டத்திலிருந்து அகன்றுள்ளார். நீங்கள் வெற்றியை கோரலாம் அல்லது இழுபறிக்கலாம் அல்லது காத்திருக்கலாம் . - வெற்றியைப் பெற்றுக்கொள் - சமன் செய்ய கோரு - உரையாடலில் மென்மையாக உரையாடு! + உங்கள் எதிராளி ஆட்டத்திலிருந்து அகன்றுள்ளார். நீங்கள் வெற்றியை உரிமைகோரலாம் அல்லது இழுபறிக்கலாம் அல்லது காத்திருக்கலாம் . + வெற்றியைப் உரிமைகோரு + இழுபறிக்க கோரு + உரையாடலில் தன்மையாக உரையாடு! இந்த் URL இல் வந்தடையும் முதலாள் உங்களோடு விளையாடுவார். வெள்ளை விலகிற்று கருப்பு விலகிற்று வெள்ளை ஆட்டத்தை விட்டது கருப்பு ஆட்டத்தை விட்டது - வெள்ளை நகரவில்லை - கருப்பு நகரவில்லை - கணினிப் பகுப்பாய்வு கோருக - கணினிப் பகுப்பாய்வு - கணினி பகுப்பாய்வு கிடைக்கும் - கணினி பகுப்பாய்வு முடக்கப்பட்டுள்ளது - பகுப்பாய்வு + வெள்ளை அடிபெயர்க்கவில்லை + கருப்பு அடிபெயர்க்கவில்லை + கணினி கூறாய்வு கோருக + கணினிக் கூறாய்வு + கணினிக் கூறாய்வு கிடைக்கும் + கணினிக் கூறாய்வு முடக்கப்பட்டுள்ளது + கூறாய்வு பலகை ஆழம் %s சேவையக பகுப்பாய்வைப் பயன்படுத்துதல் சற்று நேரம் பொறுங்கள்... @@ -65,39 +65,39 @@ மேகப்பகுப்பாய்வு ஆழமாக செல்லுங்கள் அச்சுறுத்தல்களைக் காண்பி - உங்கள் இணைய உலாவியில் - கணினி ஆராய்ச்சி - மாறுபாடு ஊக்குவித்தல் - பிரதான தொடர்ச்சி ஆக்கு + உங்கள் உலாவியில் + கணினி மதிப்பாய்வு + பிறழ்வை ஊக்குவித்தல் + முதன்மை வரியாக்கு இங்கிருந்து நீக்கு - மாறுபாடுகளைச் சுருக்கவும் - மாறுபாடுகளை விரிவாக்குக - ஆராய்ச்சியை தனியாக காமி - மாறுபாடுகள் கொண்ட PGN ஐ நகலெடு + பிறழ்வுகளைச் சுருக்கு + பிறழ்வுகளை விரிவாக்கு + பிறழ்வுகளை கட்டாயப்படுத்து + பிறழ்வுகள் PGN ஐ நகலெடு நகர்த்து - மாற்று வகை தோல்வி - மாற்று வகை வெற்றி - காய்கள் பற்றாக்குறை - காய் நகர்த்து - பிடிப்பு + திரிபுரு தோல்வி + திரிபுரு வெற்றி + உருபடிகள் பற்றாக்குறை + பணய நகர்த்தல் + பிடிபாடு மூடு - வெற்றி - இழந்து - சமநிலை அடைந்தது + வெற்றியாகிறது + இழக்கிறது + இழுபறியானது தெரியவில்லை தரவுத்தளம் - வெள்ளை / காயடைப்புநிலை / கருப்பு - சராசரி மதிப்பெண்கள்: %s - சமீபத்திய விளையாட்டு + வெள்ளை / இழுபறி / கருப்பு + சராசரி தரமதிப்பு: %s + சமீபத்திய ஆட்டங்கள் சிறந்த ஆட்டங்கள் - FIDE மதிப்பீடு %2$s முதல் %3$s வரை உள்ள %1$s+ வீரர்களின் இரண்டு மில்லியன் மேஜை மேல் ஆடிய ஆட்டங்கள் + FIDE மதிப்பீடு %2$s முதல் %3$s வரை உள்ள %1$s+ வீரர்களின் இரண்டு மில்லியன் OTB ஆட்டங்கள் - %s ஆட்டத்தில் முடிவுரை - %s ஆட்டங்களில் முடிவுரை + %s ல் பாதி நகத்தலில் முற்றுகை + %s ல் பாதி நகர்த்தல்களில் முற்றுகை - DTZ50\" ரவுண்டிங்குடன், அடுத்த பிடிப்பு அல்லது சிப்பாய் நகரும் வரை அரை நகர்வுகளின் எண்ணிக்கையின் அடிப்படையில் - எந்த அணியும் கிட்டவில்லை - அதிகபட்ச ஆழம் எட்டியது! + DTZ50\" சுற்றுகளுடன், அடுத்த பிடிபாடு அல்லது பணயம் நகரும் வரை பாதி நகர்வுகளின் எண்ணிக்கையின் அடிப்படையில் + எந்த ஆட்டங்களும் அறியப்படவில்லை + அதிகப்படியான ஆழம் எட்டியது! இன்னும் விளையாடின சதுரங்க ஆட்டத்தை காட்டுவதற்காக \"ப்ரிஃபரன்சச்\"சை மாற்றுங்கள்? தொடக்கம் திறப்புகளைஆய்வு & தரவுத் தளம் @@ -936,7 +936,7 @@ ஸ்ட்ரீமர் மேலாளர் போட்டியை ரத்து செய் போட்டி விளக்கம் - பங்கேற்பாளர்களுக்கு ஏதாவது சிறப்புச் சொல்ல விரும்புகிறீர்களா? அதை சுருக்கமாக வைக்க முயற்சி செய்யுங்கள். மார்க் டவுன் இணைப்புகள் உள்ளன: [name](https://url) + பங்கேற்பாளர்களுக்கு ஏதாவது சிறப்புச் சொல்ல விரும்புகிறீர்களா? அதை சுருக்கமாக வைக்க முயற்சி செய்யுங்கள். மார்க் டவுன் இணைப்புகள் உள்ளன: [name](https://url) கேம்கள் மதிப்பிடப்படுகின்றன மற்றும் பிளேயர்களின் மதிப்பீடுகளை பாதிக்கின்றன குழு உறுப்பினர்கள் மட்டுமே தடை இல்லை diff --git a/translation/dest/streamer/ko-KR.xml b/translation/dest/streamer/ko-KR.xml index 1269f64a5881e..38dc2aedeed29 100644 --- a/translation/dest/streamer/ko-KR.xml +++ b/translation/dest/streamer/ko-KR.xml @@ -29,8 +29,13 @@ 운영진이 스트리밍을 검토하고 있습니다. 스트리머 정보를 채우고 사진을 업로드해주세요. Lichess 스트리머가 될 준비가 되셨다면, %s + 검토를 위해 제출 체스 스트리머 페이지에서는 내 스트리밍 플랫폼에서 제공하는 언어로 시청자를 타겟팅합니다. 생방송에 사용하는 앱 또는 서비스에서 체스 스트림의 기본 언어를 올바르게 설정하세요. 트위치 유저네임 또는 URL + 트위츠 아니면 유튜브 필요합니다 + 트위츠 및 유튜브 변경사항을 확인해야 합니다. + + 검토가 진행되는 동안 스트리머 배지와 목록이 일시 중지됩니다. 최대 72시간이 걸릴 수 있습니다. 당신의 YouTube 채널ID Lichess의 스트리머 이름 diff --git a/translation/dest/streamer/pt-PT.xml b/translation/dest/streamer/pt-PT.xml index 4a13b5e019bc9..db57242cf6eeb 100644 --- a/translation/dest/streamer/pt-PT.xml +++ b/translation/dest/streamer/pt-PT.xml @@ -2,7 +2,7 @@ Streamers no Lichess Streamer no Lichess - EM DIRETO! + Ao vivo! NÃO CONECTADO A transmitir agora: %s Última stream %s @@ -29,8 +29,12 @@ A tua stream está a ser revista por moderadores. Por favor preenche a tua informação informação de streamer, e coloca uma imagem. Quando estiveres pronto para ser listado como um streamer do Lichess, %s + Enviar para revisão A página do streamer Lichess visa o seu público com o idioma fornecido pela sua plataforma de streaming. Defina o idioma padrão correto para os seus streams de xadrez no aplicativo ou serviço usado para transmitir. O teu nome de utilizador ou link na Twitch + Twitch e YouTube as alterações devem ser verificadas + + Você Streamer o selo e a listagem será suspensa enquanto a revisão estar em andamento. Isso pode levar 72 horas. O seu ID de canal do YouTube O teu nome de streamer no Lichess diff --git a/translation/dest/streamer/ru-RU.xml b/translation/dest/streamer/ru-RU.xml index 5ead51bf0bf05..cfb0ffaf137a9 100644 --- a/translation/dest/streamer/ru-RU.xml +++ b/translation/dest/streamer/ru-RU.xml @@ -29,8 +29,13 @@ Ваш стрим на проверке у модераторов. Пожалуйста, заполните информацию о стриме и загрузите изображение. Когда вы будете готовы оказаться в списке стримеров Lichess, %s + Отправить на проверку Страница стримера Lichess ориентирована на ту аудиторию, язык которой предоставлен вашей платформой. Установите для ваших шахматных стримов правильный язык по умолчанию в приложении или сервисе, который вы используете для трансляции. Ваше имя пользователя или URL на Twitch + Требуется Twitch или YouTube + Необходимо проверить изменения на Twitch и YouTube. + + Ваш значок стримера и отображение в списке будут приостановлены на время рассмотрения. Это может занять до 72 часов. ID вашего YouTube-канала Ваше имя стримера на Lichess diff --git a/translation/dest/streamer/uk-UA.xml b/translation/dest/streamer/uk-UA.xml index 68018525db29b..7708908c9152c 100644 --- a/translation/dest/streamer/uk-UA.xml +++ b/translation/dest/streamer/uk-UA.xml @@ -29,8 +29,10 @@ Вашу трансляцію перевіряють модератори. Будь ласка, заповніть інформацію про трансляцію та завантажте фотографію. Коли ви будете готові числитися як стример Lichess, %s + Відправити на перегляд Сторінка стримера Lichess націлена на глядачів за мовою, наданою вашою стримінговою платформою. Встановіть правильну мову за замовчуванням для ваших шахових трансляцій в додатку чи сервісі, який ви використовуєте для трансляцій. Ваше ім\'я користувача Twitch або посилання + Потрібний Twitch або YouTube ID вашого YouTube каналу Ваше ім\'я для трансляцій на Lichess diff --git a/translation/dest/streamer/vi-VN.xml b/translation/dest/streamer/vi-VN.xml index 36ce0f626209a..4f705c9fb7d06 100644 --- a/translation/dest/streamer/vi-VN.xml +++ b/translation/dest/streamer/vi-VN.xml @@ -29,8 +29,13 @@ Luồng của bạn đang được các điều hành viên đánh giá. Hãy điền thông tin về luồng của bạn và tải lên một ảnh. Khi bạn sẵn sàng trờ thành Streamer của Lichess, %s + Gửi để xem xét Trang Streamer Lichess hướng đến người xem là những người dùng ngôn ngữ bằng ngôn ngữ do nền tảng phát trực tuyến của bạn cung cấp. Đặt ngôn ngữ mặc định chính xác cho luồng cờ vua của bạn trong ứng dụng hoặc dịch vụ mà bạn sử dụng để phát sóng. Tên người dùng hoặc đường dẫn đến trang Twitch của bạn + Cần phải có Twitch hoặc Youtube + Các thay đổi trên Twitch và YouTube phải được xác minh. + + Huy hiệu người phát trực tiếp và danh sách của bạn sẽ bị tạm ngưng trong khi quá trình xem xét đang diễn ra. Việc này có thể mất tới 72 giờ. ID kênh YouTube của bạn Tên luồng của bạn trên Lichess diff --git a/translation/dest/streamer/zh-TW.xml b/translation/dest/streamer/zh-TW.xml index d370e79a3686b..4c5cd9d7553ad 100644 --- a/translation/dest/streamer/zh-TW.xml +++ b/translation/dest/streamer/zh-TW.xml @@ -29,8 +29,13 @@ 您的直播正在被版主審核 請填入您的直播資訊並上傳一張圖片 當您準備好要成為Lichess實況主時, %s + 提交以待審核 Lichess 實況主的頁面透過直播平台能自動針對同樣語言的觀眾推薦您的內容。請在應用程式內設定您的直播或服務的正確預設語言。 您的Twitch用戶名或是網址 + 需要 Twitch 或 YouTube 其中之一 + Twitch 和 YouTube 的更改必須通過驗證。 + +在審核過程中,您的實況主徽章和列表將被暫時停用。此過程可能需要長達 72 小時。 你的 YouTube 頻道 ID 您在Lichess上的直播名稱 diff --git a/translation/dest/study/zh-TW.xml b/translation/dest/study/zh-TW.xml index e3e24ecbd00e0..9bd95e0592045 100644 --- a/translation/dest/study/zh-TW.xml +++ b/translation/dest/study/zh-TW.xml @@ -163,4 +163,5 @@ 再玩一次 你會在這個位置上怎麼走? 恭喜!您完成了這個課程。 + %s 每頁 diff --git a/translation/dest/swiss/zh-TW.xml b/translation/dest/swiss/zh-TW.xml index 7cd7eb3c8eac6..1da30ac48bce8 100644 --- a/translation/dest/swiss/zh-TW.xml +++ b/translation/dest/swiss/zh-TW.xml @@ -106,6 +106,12 @@ 遲到加入 是的,直到超過一半的輪次開始為止 暫停 + 可以,但可能會減少輪次數量 + 連勝與狂戰士模式 + 類似於線下比賽 + 無限制且免費 + 僅限白名單內的玩家加入 + 如果此列表不為空,則未在此列表中的用戶將被禁止加入。每行填寫一個用戶名稱。 完成棋局 輪空 缺席 diff --git a/translation/dest/team/zh-TW.xml b/translation/dest/team/zh-TW.xml index 6c833ab7012d7..25b0b28c4fc4f 100644 --- a/translation/dest/team/zh-TW.xml +++ b/translation/dest/team/zh-TW.xml @@ -60,6 +60,7 @@ 您無法移除已加入錦標賽的團隊。 每隊的團長人數。他們的分數加總即是整對的分數。 您不應該在錦標賽開始後改變此值! + 隊內比賽 %s 隊之間的戰鬥 diff --git a/translation/dest/timeago/mr-IN.xml b/translation/dest/timeago/mr-IN.xml index c691a9c20e6ae..9280cf5a804a0 100644 --- a/translation/dest/timeago/mr-IN.xml +++ b/translation/dest/timeago/mr-IN.xml @@ -2,19 +2,19 @@ आत्ताच - %s सेकंद + %s सेकंदात %s सेकंद - %s मिनिट + %s मिनटात %s मिनीटांत - %s तासांमध्ये + %s तासात %s तासांमध्ये - %s दिवस + %s दिवसात %s दिवसांत @@ -26,7 +26,7 @@ %s महिन्यांत - %s वर्ष + %s वर्षात %s वर्षात आत्ताच @@ -35,23 +35,23 @@ %s मिनिटांआधी - %s तासाआधी + %s तासांपूर्वी %s तासांआधी - %s दिवसाआधी + %s दिवसांपूर्वी %s दिवसांआधी - %s आठवड्यापूर्वी + %s आठवड्यांपूर्वी %s आठवड्यांपूर्वी - %s महिन्यापूर्वी + %s महिन्यांपूर्वी %s महिन्यांपूर्वी - %s वर्षापूर्वी + %s वर्षांपूर्वी %s वर्षांपूर्वी From a7a7773cf338047fa6146ec84176c1b75e6bc8fe Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 22 Dec 2024 08:14:01 +0100 Subject: [PATCH 16/18] delete broken translation --- translation/dest/activity/ta-IN.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/translation/dest/activity/ta-IN.xml b/translation/dest/activity/ta-IN.xml index 93fac056f1b59..5c214954e62cd 100644 --- a/translation/dest/activity/ta-IN.xml +++ b/translation/dest/activity/ta-IN.xml @@ -42,10 +42,6 @@ %s பிளேயரைப் பின்தொடரத் தொடங்கியது %s வீரர்களைப் பின்தொடரத் தொடங்கியது - - %ஒரு புதிய பின்தொடர்பவர் கிடைத்தது - %s புதிய பின்தொடர்பவர்களைப் பெற்றார் - %s ஒரே நேரத்தில் கண்காட்சி நடத்தப்பட்டது %s ஒரே நேரத்தில் கண்காட்சிகள் நடத்தப்பட்டன From 2ad5999772ae7aeb6b3b9a4f05dacd8faaf12889 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 22 Dec 2024 08:46:59 +0100 Subject: [PATCH 17/18] reduce broadcast base period after adding etag support --- modules/relay/src/main/RelayFetch.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/relay/src/main/RelayFetch.scala b/modules/relay/src/main/RelayFetch.scala index c20682af57baa..5327b28e38059 100644 --- a/modules/relay/src/main/RelayFetch.scala +++ b/modules/relay/src/main/RelayFetch.scala @@ -176,7 +176,7 @@ final private class RelayFetch( private def dynamicPeriod(tour: RelayTour, round: RelayRound, upstream: Sync.Upstream) = Seconds: val base = - if upstream.hasLcc then 6 + if upstream.hasLcc then 5 else if upstream.isRound then 10 // uses push so no need to pull often else 2 base * { From cd27d58a84ec515f520718c82a7235fa96c7d695 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 22 Dec 2024 08:48:49 +0100 Subject: [PATCH 18/18] redirect to own /recap --- app/controllers/Recap.scala | 6 ++---- conf/routes | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/controllers/Recap.scala b/app/controllers/Recap.scala index cff58e10442a1..65153c7129b2a 100644 --- a/app/controllers/Recap.scala +++ b/app/controllers/Recap.scala @@ -30,8 +30,6 @@ final class Recap(env: Env) extends LilaController(env): res <- f(using ctx.updatePref(_.forceDarkBg))(user)(av) yield res if me.is(username) then proceed(me) - else - Found(env.user.api.byId(username)): user => - val canView = isGranted(_.SeeInsight) || !env.net.isProd - canView.so(proceed(user)) + else if isGranted(_.SeeInsight) || !env.net.isProd then Found(env.user.api.byId(username))(proceed) + else Redirect(routes.Recap.home).toFuccess } diff --git a/conf/routes b/conf/routes index bdd157fd5e94e..b74dc04dc0ba6 100644 --- a/conf/routes +++ b/conf/routes @@ -806,7 +806,7 @@ GET /tutor/:username/:perf/phase controllers.Tutor.phases(username: UserSt GET /tutor/:username/:perf/time controllers.Tutor.time(username: UserStr, perf: PerfKey) # Recap -GET /recap controllers.Recap.home() +GET /recap controllers.Recap.home GET /recap/:username controllers.Recap.user(username: UserStr) # OAuth