Skip to content

Commit 0b47361

Browse files
committed
Closes lichess-org#15009. Study chapter clock editing (inline set/clear).
1 parent 21dc386 commit 0b47361

15 files changed

Lines changed: 497 additions & 50 deletions

File tree

modules/coreI18n/src/main/key.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2591,6 +2591,10 @@ object I18nKey:
25912591
val `like`: I18nKey = "study:like"
25922592
val `unlike`: I18nKey = "study:unlike"
25932593
val `newTag`: I18nKey = "study:newTag"
2594+
val `addOrEditClockTime`: I18nKey = "study:addOrEditClockTime"
2595+
val `clockTimeFormat`: I18nKey = "study:clockTimeFormat"
2596+
val `clockTimePlaceholder`: I18nKey = "study:clockTimePlaceholder"
2597+
val `turnOnRecToSaveClockTimes`: I18nKey = "study:turnOnRecToSaveClockTimes"
25942598
val `commentThisPosition`: I18nKey = "study:commentThisPosition"
25952599
val `commentThisMove`: I18nKey = "study:commentThisMove"
25962600
val `annotateWithGlyphs`: I18nKey = "study:annotateWithGlyphs"

modules/relay/src/main/RelaySync.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ final private class RelaySync(
105105
.filter: c =>
106106
existing.clock.forall: prev =>
107107
~c.trust && c.centis != prev.centis
108-
.so: c =>
108+
.foreach: c =>
109109
studyApi.setClock(
110110
studyId = study.id,
111111
position = Position(chapter, path).ref,
112-
clock = c
112+
clock = c.some
113113
)(by)
114114
path -> none
115115
case (found, _) => found

modules/study/src/main/ChapterRepo.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,19 @@ final class ChapterRepo(val coll: AsyncColl)(using Executor, akka.stream.Materia
126126
def setClockAndDenorm(
127127
chapter: Chapter,
128128
path: UciPath,
129-
clock: Clock,
129+
clock: Option[Clock],
130130
denorm: Option[Chapter.BothClocks]
131131
) =
132-
val updateNode = $doc(pathToField(path, F.clock) -> clock)
133-
val updateDenorm = denorm.map(clocks => $doc("denorm.clocks" -> clocks))
132+
val modifier = clock match
133+
case None =>
134+
$unset(pathToField(path, F.clock)) ++ denorm.fold($empty)(clocks => $set("denorm.clocks" -> clocks))
135+
case Some(c) =>
136+
val updateNode = $doc(pathToField(path, F.clock) -> c)
137+
val updateDenorm = denorm.map(clocks => $doc("denorm.clocks" -> clocks))
138+
$set(updateDenorm.foldLeft(updateNode)(_ ++ _))
134139
coll:
135140
_.update
136-
.one(
137-
$id(chapter.id) ++ $doc(path.toDbField.$exists(true)),
138-
$set(updateDenorm.foldLeft(updateNode)(_ ++ _))
139-
)
141+
.one($id(chapter.id) ++ $doc(path.toDbField.$exists(true)), modifier)
140142
.void
141143

142144
def forceVariation(force: Boolean) = setNodeValue(F.forceVariation, force.option(true))

modules/study/src/main/StudyApi.scala

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -425,18 +425,21 @@ final class StudyApi(
425425
reloadSriBecauseOf(study, who.sri, chapter.id)
426426
fufail(s"Invalid setShapes $position $shapes")
427427

428-
def setClock(studyId: StudyId, position: Position.Ref, clock: Clock)(who: Who): Funit =
428+
def setClock(studyId: StudyId, position: Position.Ref, clock: Option[Clock])(who: Who): Funit =
429429
sequenceStudyWithChapter(studyId, position.chapterId):
430-
doSetClock(_, position, clock)(who)
430+
case sc @ Study.WithChapter(study, chapter) =>
431+
Contribute(who.u, study):
432+
if study.isRelay then fufail("Cannot edit clock on relay chapter")
433+
else doSetClock(sc, position, clock)(who)
431434

432-
private def doSetClock(sc: Study.WithChapter, position: Position.Ref, clock: Clock)(
435+
private def doSetClock(sc: Study.WithChapter, position: Position.Ref, clock: Option[Clock])(
433436
who: Who
434437
): Funit =
435-
sc.chapter.setClock(clock.some, position.path) match
438+
sc.chapter.setClock(clock, position.path) match
436439
case Some(chapter, newCurrentClocks) =>
437440
setStudyUpdated(sc.study)
438441
for _ <- chapterRepo.setClockAndDenorm(chapter, position.path, clock, newCurrentClocks)
439-
yield sendTo(sc.study.id)(_.setClock(position, clock.centis.some, newCurrentClocks))
442+
yield sendTo(sc.study.id)(_.setClock(position, clock.map(_.centis), newCurrentClocks))
440443
case None =>
441444
reloadSriBecauseOf(sc.study, who.sri, position.chapterId)
442445
fufail(s"Invalid setClock $position $clock")
@@ -472,7 +475,7 @@ final class StudyApi(
472475
.setRootClockFromTags(chapter)
473476
.so: c =>
474477
c.root.clock.so: clock =>
475-
doSetClock(Study.WithChapter(study, c), Position(c, UciPath.root).ref, clock)(who)
478+
doSetClock(Study.WithChapter(study, c), Position(c, UciPath.root).ref, clock.some)(who)
476479
yield sendTo(study.id)(_.setTags(chapter.id, chapter.tags, who))
477480

478481
def setComment(studyId: StudyId, position: Position.Ref, text: CommentStr)(who: Who) =

modules/study/src/main/StudySocket.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import lila.common.Bus
1010
import lila.common.Json.{ *, given }
1111
import lila.room.RoomSocket.{ Protocol as RP, * }
1212
import lila.core.socket.{ protocol as P, * }
13-
import lila.tree.Branch
13+
import lila.tree.{ Branch, Clock }
1414
import lila.tree.Node.{ Comment, Gamebook, Shape, Shapes }
1515
import lila.core.study.Visibility
1616
import cats.mtl.Handle.*
@@ -198,6 +198,18 @@ final private class StudySocket(
198198
.foreach: id =>
199199
applyWho(api.deleteComment(studyId, position.ref, Comment.Id(id)))
200200

201+
case "setClock" =>
202+
logger.info(s"setClock received studyId=$studyId o=$o")
203+
reading[AtPosition](o): position =>
204+
if (o \ "d" \ "clear").asOpt[Boolean].contains(true) then
205+
applyWho(api.setClock(studyId, position.ref, None)(_))
206+
else
207+
(o \ "d" \ "centis")
208+
.asOpt[Int]
209+
.filter(_ >= 0)
210+
.foreach: centis =>
211+
applyWho(api.setClock(studyId, position.ref, Clock(Centis(centis), false.some).some)(_))
212+
201213
case "setGamebook" =>
202214
reading[AtPosition](o): position =>
203215
(o \ "d" \ "gamebook")

translation/source/study.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
<string name="like">Like</string>
4545
<string name="unlike">Unlike</string>
4646
<string name="newTag">New tag</string>
47+
<string name="addOrEditClockTime">Add or edit clock time</string>
48+
<string name="clockTimeFormat">Time format: H:MM:SS (e.g. 1:03:40), MM:SS (3:40), or seconds (40). Optional tenths: 40.5 or 3:40.2. Enter - to clear.</string>
49+
<string name="clockTimePlaceholder">H:MM:SS</string>
50+
<string name="turnOnRecToSaveClockTimes">Turn on REC (record button below the board) to save clock times.</string>
4751
<string name="commentThisPosition">Comment on this position</string>
4852
<string name="commentThisMove">Comment on this move</string>
4953
<string name="annotateWithGlyphs">Annotate with glyphs</string>

ui/@types/lichess/i18n.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5035,6 +5035,8 @@ interface I18n {
50355035
addMembersText: I18nFormat;
50365036
/** Add a new chapter */
50375037
addNewChapter: string;
5038+
/** Add or edit clock time */
5039+
addOrEditClockTime: string;
50385040
/** Allow cloning */
50395041
allowCloning: string;
50405042
/** All studies */
@@ -5083,6 +5085,10 @@ interface I18n {
50835085
clearChat: string;
50845086
/** Clear variations */
50855087
clearVariations: string;
5088+
/** Time format: H:MM:SS (e.g. 1:03:40), MM:SS (3:40), or seconds (40). Optional tenths: 40.5 or 3:40.2. Enter - to clear. */
5089+
clockTimeFormat: string;
5090+
/** H:MM:SS */
5091+
clockTimePlaceholder: string;
50865092
/** Clone */
50875093
cloneStudy: string;
50885094
/** Click the %s button, or right click on the move list on the right.<br>Comments are shared and saved. */
@@ -5355,6 +5361,8 @@ interface I18n {
53555361
timeTrouble: string;
53565362
/** Topics */
53575363
topics: string;
5364+
/** Turn on REC (record button below the board) to save clock times. */
5365+
turnOnRecToSaveClockTimes: string;
53585366
/** Unclear position */
53595367
unclearPosition: string;
53605368
/** Unlike */

ui/analyse/css/_player-clock.scss

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ $clock-height: 20px;
3939
height: $clock-height;
4040
font-weight: bold;
4141
text-align: center;
42+
/* Reserve border space so empty-editable dashed border doesn't cause 1px shift when .active toggles */
43+
border: 1px solid transparent;
4244

4345
&.active {
4446
background: $m-primary_bg--mix-30;
@@ -53,6 +55,61 @@ $clock-height: 20px;
5355
margin-inline-end: 0.4em;
5456
color: $c-accent;
5557
}
58+
59+
&--editable {
60+
cursor: pointer;
61+
border: 1px dashed $c-primary;
62+
@media (hover: hover) {
63+
&:hover {
64+
/* Same effect as move sidebar; --c-base is only set inside .tview2 so use $c-font here */
65+
background: color-mix(in srgb, $c-font 15%, transparent);
66+
}
67+
}
68+
}
69+
70+
&--editing {
71+
flex-shrink: 0;
72+
min-width: 0; /* allow input to size */
73+
.analyse__clock-input {
74+
min-width: 10em;
75+
width: 10em;
76+
padding: 0 0.4em;
77+
font: inherit;
78+
font-weight: bold;
79+
text-align: center;
80+
border: 1px solid $c-border;
81+
border-radius: 3px;
82+
background: $c-bg-box;
83+
box-sizing: border-box;
84+
}
85+
}
86+
87+
.analyse__clock-input--error {
88+
border-color: var(--c-bad, #c23) !important;
89+
animation: clock-shake 0.3s ease;
90+
&:focus {
91+
/* Inset box-shadow so the error ring is visible when the top strip is partially covered by the board */
92+
outline: none;
93+
box-shadow: inset 0 0 0 2px var(--c-bad, #c23);
94+
}
95+
}
96+
}
97+
98+
@keyframes clock-shake {
99+
0%,
100+
100% {
101+
transform: translateX(0);
102+
}
103+
25% {
104+
transform: translateX(-3px);
105+
}
106+
75% {
107+
transform: translateX(3px);
108+
}
109+
}
110+
111+
.analyse__clock-placeholder {
112+
opacity: 0.7;
56113
}
57114

58115
.material {

ui/analyse/css/study/_player.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ $player-height: 1.6rem;
7070
box-shadow: none;
7171
font-size: 1.2em;
7272
font-weight: normal;
73+
74+
&--editing {
75+
padding-inline-end: 0;
76+
}
7377
}
7478

7579
&-bot .analyse__clock {

ui/analyse/src/socket.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface StudySocketSendParams {
3131
shapes: (d: ReqPosition & { shapes: Shape[] }) => void;
3232
setComment: (d: ReqPosition & { id?: string; text: string }) => void;
3333
deleteComment: (d: ReqPosition & { id: string }) => void;
34+
setClock: (d: ReqPosition & { centis?: number; clear?: boolean }) => void;
3435
setGamebook: (d: ReqPosition & { gamebook: { deviation?: string; hint?: string } }) => void;
3536
toggleGlyph: (d: ReqPosition & { id: number }) => void;
3637
explorerGame: (d: ReqPosition & { gameId: string; insert: boolean }) => void;

0 commit comments

Comments
 (0)