diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index 9cd961a98e6b..83b8ce92adbb 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -2396,6 +2396,7 @@ object I18nKey: val `allSyncMembersRemainOnTheSamePosition`: I18nKey = "study:allSyncMembersRemainOnTheSamePosition" val `shareChanges`: I18nKey = "study:shareChanges" val `playing`: I18nKey = "study:playing" + val `showResults`: I18nKey = "study:showResults" val `showEvalBar`: I18nKey = "study:showEvalBar" val `first`: I18nKey = "study:first" val `previous`: I18nKey = "study:previous" diff --git a/translation/source/study.xml b/translation/source/study.xml index 1c121d8022f2..26f595f0d811 100644 --- a/translation/source/study.xml +++ b/translation/source/study.xml @@ -53,6 +53,7 @@ All SYNC members remain on the same position Share changes with spectators and save them on the server Playing + Results Evaluation bars First Previous diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 4fe2d2fee00b..8701c5cd40dd 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -4867,6 +4867,8 @@ interface I18n { shareChanges: string; /** Evaluation bars */ showEvalBar: string; + /** Results */ + showResults: string; /** Spectator */ spectator: string; /** Start */ diff --git a/ui/analyse/css/study/panel/_multiboard.scss b/ui/analyse/css/study/panel/_multiboard.scss index d27605c2e77c..6f293821c512 100644 --- a/ui/analyse/css/study/panel/_multiboard.scss +++ b/ui/analyse/css/study/panel/_multiboard.scss @@ -47,12 +47,14 @@ } .playing, - .eval { + .eval, + .results { cursor: pointer; } .playing input, - .eval input { + .eval input, + .results input { vertical-align: middle; margin-inline-end: 3px; } @@ -164,3 +166,8 @@ border-radius: 4px 0 0 4px; } } + +.empty-boards-note { + margin-bottom: 0.5em; + color: var(--c-font-dim); +} diff --git a/ui/analyse/src/study/multiBoard.ts b/ui/analyse/src/study/multiBoard.ts index 85ce3b0d0e03..27d9d9f858ad 100644 --- a/ui/analyse/src/study/multiBoard.ts +++ b/ui/analyse/src/study/multiBoard.ts @@ -12,11 +12,12 @@ import { type StudyChapters, gameLinkAttrs, gameLinksListener } from './studyCha import { playerFed } from './playerBars'; import { userTitle } from 'common/userLink'; import { h } from 'snabbdom'; -import { storage } from 'common/storage'; +import { storage, storedBooleanProp, StoredProp } from 'common/storage'; import { Chessground as makeChessground } from 'chessground'; export class MultiBoardCtrl { playing: Toggle; + showResults: StoredProp; teamSelect: Prop = prop(''); page: number = 1; maxPerPageStorage = storage.make('study.multiBoard.maxPerPage'); @@ -28,6 +29,7 @@ export class MultiBoardCtrl { readonly redraw: () => void, ) { this.playing = toggle(false, this.redraw); + this.showResults = storedBooleanProp('study.showResults', true); if (this.initialTeamSelect) this.onChapterChange(this.initialTeamSelect); } @@ -102,8 +104,16 @@ export function view(ctrl: MultiBoardCtrl, study: StudyCtrl): MaybeVNode { ctrl.multiCloudEval && h('label.eval', [renderEvalToggle(ctrl.multiCloudEval), i18n.study.showEvalBar]), renderPlayingToggle(ctrl), + renderShowResultsToggle(ctrl), ]), ]), + !ctrl.showResults() + ? h( + 'div.empty-boards-note', + { attrs: { 'data-icon': licon.InfoCircle } }, + ' Since you chose to hide the results, all the preview boards are empty to avoid spoilers.', + ) + : undefined, h( 'div.now-playing', { @@ -111,7 +121,7 @@ export function view(ctrl: MultiBoardCtrl, study: StudyCtrl): MaybeVNode { insert: gameLinksListener(study.chapterSelect), }, }, - pager.currentPageResults.map(makePreview(baseUrl, study.vm.chapterId, cloudEval)), + pager.currentPageResults.map(makePreview(baseUrl, study.vm.chapterId, cloudEval, ctrl.showResults())), ), ]); } @@ -176,6 +186,15 @@ const renderPlayingToggle = (ctrl: MultiBoardCtrl): MaybeVNode => i18n.study.playing, ]); +const renderShowResultsToggle = (ctrl: MultiBoardCtrl): MaybeVNode => + h('label.results', [ + h('input', { + attrs: { type: 'checkbox', checked: ctrl.showResults() }, + hook: bind('change', e => ctrl.showResults((e.target as HTMLInputElement).checked), ctrl.redraw), + }), + i18n.study.showResults, + ]); + const previewToCgConfig = (cp: ChapterPreview): CgConfig => ({ fen: cp.fen, lastMove: uciToMove(cp.lastMove), @@ -184,8 +203,18 @@ const previewToCgConfig = (cp: ChapterPreview): CgConfig => ({ }); const makePreview = - (roundPath: string, current: ChapterId, cloudEval?: MultiCloudEval) => (preview: ChapterPreview) => { + (roundPath: string, current: ChapterId, cloudEval?: MultiCloudEval, showResults?: boolean) => + (preview: ChapterPreview) => { const orientation = preview.orientation || 'white'; + const baseConfig = { + coordinates: false, + viewOnly: true, + orientation, + drawable: { + enabled: false, + visible: false, + }, + }; return h( `a.mini-game.is2d.chap-${preview.id}`, { @@ -193,39 +222,45 @@ const makePreview = attrs: gameLinkAttrs(roundPath, preview), }, [ - boardPlayer(preview, CgOpposite(orientation)), + boardPlayer(preview, CgOpposite(orientation), showResults), h('span.cg-gauge', [ - cloudEval && verticalEvalGauge(preview, cloudEval), + showResults ? cloudEval && verticalEvalGauge(preview, cloudEval) : undefined, h( 'span.mini-game__board', h('span.cg-wrap', { hook: { insert(vnode) { const el = vnode.elm as HTMLElement; - vnode.data!.cg = makeChessground(el, { - ...previewToCgConfig(preview), - coordinates: false, - viewOnly: true, - orientation, - drawable: { - enabled: false, - visible: false, - }, - }); + vnode.data!.cg = showResults + ? makeChessground(el, { + ...previewToCgConfig(preview), + ...baseConfig, + }) + : makeChessground(el, { + fen: '8/8/8/8/8/8/8/8', + ...baseConfig, + }); vnode.data!.fen = preview.fen; }, postpatch(old, vnode) { if (old.data!.fen !== preview.fen) { old.data!.cg?.set(previewToCgConfig(preview)); } + // In this case, showResults was set to true but the cg fen is still on the initial pos + if (showResults && old.data!.cg.fen != old.data!.cg.getFen()) { + old.data!.cg.set(previewToCgConfig(preview)); + } vnode.data!.fen = preview.fen; - vnode.data!.cg = old.data!.cg; + const el = vnode.elm as HTMLElement; + vnode.data!.cg = showResults + ? old.data!.cg + : makeChessground(el, { fen: '8/8/8/8/8/8/8/8', ...baseConfig }); }, }, }), ), ]), - boardPlayer(preview, orientation), + boardPlayer(preview, orientation, showResults), ], ); }; @@ -300,12 +335,12 @@ const computeTimeLeft = (preview: ChapterPreview, color: Color): number | undefi } else return; }; -const boardPlayer = (preview: ChapterPreview, color: Color) => { +const boardPlayer = (preview: ChapterPreview, color: Color, showResults?: boolean) => { const outcome = preview.status && preview.status !== '*' ? preview.status : undefined; const player = preview.players?.[color], score = outcome?.split('-')[color === 'white' ? 0 : 1]; return h('span.mini-game__player', [ player && renderUser(player), - score ? h('span.mini-game__result', score) : renderClock(preview, color), + showResults ? (score ? h('span.mini-game__result', score) : renderClock(preview, color)) : undefined, ]); }; diff --git a/ui/analyse/src/study/relay/relayGames.ts b/ui/analyse/src/study/relay/relayGames.ts index 5d7869b6ff82..ed50e447e6ab 100644 --- a/ui/analyse/src/study/relay/relayGames.ts +++ b/ui/analyse/src/study/relay/relayGames.ts @@ -12,6 +12,7 @@ export const gamesList = (study: StudyCtrl, relay: RelayCtrl) => { const chapters = study.chapters.list.all(); const cloudEval = study.multiCloudEval?.thisIfShowEval(); const roundPath = relay.roundPath(); + const showResults = study.multiBoard.showResults(); return h( 'div.relay-games', { @@ -45,7 +46,7 @@ export const gamesList = (study: StudyCtrl, relay: RelayCtrl) => { class: { 'relay-game--current': c.id === study.data.chapter.id }, }, [ - cloudEval && verticalEvalGauge(c, cloudEval), + showResults ? cloudEval && verticalEvalGauge(c, cloudEval) : undefined, h( 'span.relay-game__players', players.map((p, i) => { @@ -58,7 +59,7 @@ export const gamesList = (study: StudyCtrl, relay: RelayCtrl) => { playerFed(p.fed), h('span.name', [userTitle(p), p.name]), ]), - h(s === '1' ? 'good' : s === '0' ? 'bad' : 'status', [s]), + showResults ? h(s === '1' ? 'good' : s === '0' ? 'bad' : 'status', [s]) : null, ] : [h('span.mini-game__user', h('span.name', 'Unknown player'))], ); diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index e93fd1fdd800..87368f07abd5 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -361,7 +361,9 @@ export default class StudyCtrl { } else { nextPath = sameChapter ? prevPath - : this.data.chapter.relayPath || this.chapters.localPaths[this.vm.chapterId] || treePath.root; + : this.relay && !this.multiBoard.showResults() + ? treePath.root + : this.data.chapter.relayPath || this.chapters.localPaths[this.vm.chapterId] || treePath.root; } // path could be gone (because of subtree deletion), go as far as possible