Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2591,6 +2591,10 @@ object I18nKey:
val `like`: I18nKey = "study:like"
val `unlike`: I18nKey = "study:unlike"
val `newTag`: I18nKey = "study:newTag"
val `addOrEditClockTime`: I18nKey = "study:addOrEditClockTime"
val `clockTimeFormat`: I18nKey = "study:clockTimeFormat"
val `clockTimePlaceholder`: I18nKey = "study:clockTimePlaceholder"
val `turnOnRecToSaveClockTimes`: I18nKey = "study:turnOnRecToSaveClockTimes"
val `commentThisPosition`: I18nKey = "study:commentThisPosition"
val `commentThisMove`: I18nKey = "study:commentThisMove"
val `annotateWithGlyphs`: I18nKey = "study:annotateWithGlyphs"
Expand Down
4 changes: 2 additions & 2 deletions modules/relay/src/main/RelaySync.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ final private class RelaySync(
.filter: c =>
existing.clock.forall: prev =>
~c.trust && c.centis != prev.centis
.so: c =>
.foreach: c =>
studyApi.setClock(
studyId = study.id,
position = Position(chapter, path).ref,
clock = c
clock = c.some
)(by)
path -> none
case (found, _) => found
Expand Down
16 changes: 9 additions & 7 deletions modules/study/src/main/ChapterRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,19 @@ final class ChapterRepo(val coll: AsyncColl)(using Executor, akka.stream.Materia
def setClockAndDenorm(
chapter: Chapter,
path: UciPath,
clock: Clock,
clock: Option[Clock],
denorm: Option[Chapter.BothClocks]
) =
val updateNode = $doc(pathToField(path, F.clock) -> clock)
val updateDenorm = denorm.map(clocks => $doc("denorm.clocks" -> clocks))
val modifier = clock match
case None =>
$unset(pathToField(path, F.clock)) ++ denorm.fold($empty)(clocks => $set("denorm.clocks" -> clocks))
case Some(c) =>
val updateNode = $doc(pathToField(path, F.clock) -> c)
val updateDenorm = denorm.map(clocks => $doc("denorm.clocks" -> clocks))
$set(updateDenorm.foldLeft(updateNode)(_ ++ _))
coll:
_.update
.one(
$id(chapter.id) ++ $doc(path.toDbField.$exists(true)),
$set(updateDenorm.foldLeft(updateNode)(_ ++ _))
)
.one($id(chapter.id) ++ $doc(path.toDbField.$exists(true)), modifier)
.void

def forceVariation(force: Boolean) = setNodeValue(F.forceVariation, force.option(true))
Expand Down
15 changes: 9 additions & 6 deletions modules/study/src/main/StudyApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -425,18 +425,21 @@ final class StudyApi(
reloadSriBecauseOf(study, who.sri, chapter.id)
fufail(s"Invalid setShapes $position $shapes")

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

private def doSetClock(sc: Study.WithChapter, position: Position.Ref, clock: Clock)(
private def doSetClock(sc: Study.WithChapter, position: Position.Ref, clock: Option[Clock])(
who: Who
): Funit =
sc.chapter.setClock(clock.some, position.path) match
sc.chapter.setClock(clock, position.path) match
case Some(chapter, newCurrentClocks) =>
setStudyUpdated(sc.study)
for _ <- chapterRepo.setClockAndDenorm(chapter, position.path, clock, newCurrentClocks)
yield sendTo(sc.study.id)(_.setClock(position, clock.centis.some, newCurrentClocks))
yield sendTo(sc.study.id)(_.setClock(position, clock.map(_.centis), newCurrentClocks))
case None =>
reloadSriBecauseOf(sc.study, who.sri, position.chapterId)
fufail(s"Invalid setClock $position $clock")
Expand Down Expand Up @@ -472,7 +475,7 @@ final class StudyApi(
.setRootClockFromTags(chapter)
.so: c =>
c.root.clock.so: clock =>
doSetClock(Study.WithChapter(study, c), Position(c, UciPath.root).ref, clock)(who)
doSetClock(Study.WithChapter(study, c), Position(c, UciPath.root).ref, clock.some)(who)
yield sendTo(study.id)(_.setTags(chapter.id, chapter.tags, who))

def setComment(studyId: StudyId, position: Position.Ref, text: CommentStr)(who: Who) =
Expand Down
14 changes: 13 additions & 1 deletion modules/study/src/main/StudySocket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import lila.common.Bus
import lila.common.Json.{ *, given }
import lila.room.RoomSocket.{ Protocol as RP, * }
import lila.core.socket.{ protocol as P, * }
import lila.tree.Branch
import lila.tree.{ Branch, Clock }
import lila.tree.Node.{ Comment, Gamebook, Shape, Shapes }
import lila.core.study.Visibility
import cats.mtl.Handle.*
Expand Down Expand Up @@ -198,6 +198,18 @@ final private class StudySocket(
.foreach: id =>
applyWho(api.deleteComment(studyId, position.ref, Comment.Id(id)))

case "setClock" =>
logger.info(s"setClock received studyId=$studyId o=$o")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug

reading[AtPosition](o): position =>
if (o \ "d" \ "clear").asOpt[Boolean].contains(true) then
applyWho(api.setClock(studyId, position.ref, None)(_))
else
(o \ "d" \ "centis")
.asOpt[Int]
.filter(_ >= 0)
.foreach: centis =>
applyWho(api.setClock(studyId, position.ref, Clock(Centis(centis), false.some).some)(_))

case "setGamebook" =>
reading[AtPosition](o): position =>
(o \ "d" \ "gamebook")
Expand Down
4 changes: 4 additions & 0 deletions translation/source/study.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<string name="like">Like</string>
<string name="unlike">Unlike</string>
<string name="newTag">New tag</string>
<string name="addOrEditClockTime">Add or edit clock time</string>
<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>
<string name="clockTimePlaceholder">H:MM:SS</string>
<string name="turnOnRecToSaveClockTimes">Turn on REC (record button below the board) to save clock times.</string>
<string name="commentThisPosition">Comment on this position</string>
<string name="commentThisMove">Comment on this move</string>
<string name="annotateWithGlyphs">Annotate with glyphs</string>
Expand Down
8 changes: 8 additions & 0 deletions ui/@types/lichess/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5035,6 +5035,8 @@ interface I18n {
addMembersText: I18nFormat;
/** Add a new chapter */
addNewChapter: string;
/** Add or edit clock time */
addOrEditClockTime: string;
/** Allow cloning */
allowCloning: string;
/** All studies */
Expand Down Expand Up @@ -5083,6 +5085,10 @@ interface I18n {
clearChat: string;
/** Clear variations */
clearVariations: string;
/** 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. */
clockTimeFormat: string;
/** H:MM:SS */
clockTimePlaceholder: string;
/** Clone */
cloneStudy: string;
/** Click the %s button, or right click on the move list on the right.<br>Comments are shared and saved. */
Expand Down Expand Up @@ -5355,6 +5361,8 @@ interface I18n {
timeTrouble: string;
/** Topics */
topics: string;
/** Turn on REC (record button below the board) to save clock times. */
turnOnRecToSaveClockTimes: string;
/** Unclear position */
unclearPosition: string;
/** Unlike */
Expand Down
57 changes: 57 additions & 0 deletions ui/analyse/css/_player-clock.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ $clock-height: 20px;
height: $clock-height;
font-weight: bold;
text-align: center;
/* Reserve border space so empty-editable dashed border doesn't cause 1px shift when .active toggles */
border: 1px solid transparent;

&.active {
background: $m-primary_bg--mix-30;
Expand All @@ -53,6 +55,61 @@ $clock-height: 20px;
margin-inline-end: 0.4em;
color: $c-accent;
}

&--editable {
cursor: pointer;
border: 1px dashed $c-primary;
@media (hover: hover) {
&:hover {
/* Same effect as move sidebar; --c-base is only set inside .tview2 so use $c-font here */
background: color-mix(in srgb, $c-font 15%, transparent);
}
}
}

&--editing {
flex-shrink: 0;
min-width: 0; /* allow input to size */
.analyse__clock-input {
min-width: 10em;
width: 10em;
padding: 0 0.4em;
font: inherit;
font-weight: bold;
text-align: center;
border: 1px solid $c-border;
border-radius: 3px;
background: $c-bg-box;
box-sizing: border-box;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superfluous

}
}

.analyse__clock-input--error {
border-color: var(--c-bad, #c23) !important;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just $c-bad

animation: clock-shake 0.3s ease;
&:focus {
/* Inset box-shadow so the error ring is visible when the top strip is partially covered by the board */
outline: none;
box-shadow: inset 0 0 0 2px var(--c-bad, #c23);
}
}
}

@keyframes clock-shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-3px);
}
75% {
transform: translateX(3px);
}
}

.analyse__clock-placeholder {
opacity: 0.7;
}

.material {
Expand Down
4 changes: 4 additions & 0 deletions ui/analyse/css/study/_player.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ $player-height: 1.6rem;
box-shadow: none;
font-size: 1.2em;
font-weight: normal;

&--editing {
padding-inline-end: 0;
}
}

&-bot .analyse__clock {
Expand Down
1 change: 1 addition & 0 deletions ui/analyse/src/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface StudySocketSendParams {
shapes: (d: ReqPosition & { shapes: Shape[] }) => void;
setComment: (d: ReqPosition & { id?: string; text: string }) => void;
deleteComment: (d: ReqPosition & { id: string }) => void;
setClock: (d: ReqPosition & { centis?: number; clear?: boolean }) => void;
setGamebook: (d: ReqPosition & { gamebook: { deviation?: string; hint?: string } }) => void;
toggleGlyph: (d: ReqPosition & { id: number }) => void;
explorerGame: (d: ReqPosition & { gameId: string; insert: boolean }) => void;
Expand Down
28 changes: 27 additions & 1 deletion ui/analyse/src/study/studyCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default class StudyCtrl {
nonRelayRecMapProp = storedMap<boolean>('study.rec', 100, () => true);
chapterFlipMapProp = storedMap<boolean>('chapter.flip', 400, () => false);
arrowHistory: Shape[][] = [];
clockEdit: { slot: 'top' | 'bottom'; path: string; value: string; error?: boolean } | null = null;
data: StudyData;
vm: StudyVm;
notif: NotifCtrl;
Expand Down Expand Up @@ -317,6 +318,31 @@ export default class StudyCtrl {
ch: this.vm.chapterId,
});

openClockEdit = (slot: 'top' | 'bottom', path: string, value: string) => {
this.ctrl.autoplay.stop();
this.clockEdit = { slot, path, value };
this.ctrl.redraw();
};

setClockEditValue = (value: string) => {
if (this.clockEdit) {
this.clockEdit = { ...this.clockEdit, value, error: false };
this.ctrl.redraw();
}
};

setClockEditError = (error: boolean) => {
if (this.clockEdit) {
this.clockEdit = { ...this.clockEdit, error };
this.ctrl.redraw();
}
};

closeClockEdit = () => {
this.clockEdit = null;
this.ctrl.redraw();
};

isGamebookPlay = () =>
this.data.chapter.gamebook &&
this.vm.gamebookOverride !== 'analyse' &&
Expand Down Expand Up @@ -863,7 +889,7 @@ export default class StudyCtrl {
this.setMemberActive(who);
if (d.relayClocks) this.relay?.setClockToChapterPreview(d, d.relayClocks);
if (this.wrongChapter(d)) return;
this.ctrl.tree.setClockAt(d.c, position.path);
this.ctrl.tree.setClockAt(d.c ?? undefined, position.path);
this.redraw();
},
forceVariation: d => {
Expand Down
43 changes: 43 additions & 0 deletions ui/analyse/src/view/clockParse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/** Parse time string (H:MM:SS, MM:SS, M:SS, SS, or SS.t for tenths) to centis. Returns undefined if invalid. */
export function parseTimeToCentis(str: string): number | undefined {
const s = str.trim();
if (s === '' || s === '--:--') return undefined;
const parts = s.split(':').map(p => p.trim());
if (parts.some(p => p === '')) return undefined;
const lastPart = parts[parts.length - 1];
const lastNum = parseFloat(lastPart);
if (isNaN(lastNum) || lastNum < 0) return undefined;
const wholeParts = parts.slice(0, -1).map(p => parseInt(p, 10));
if (wholeParts.some(n => isNaN(n) || n < 0)) return undefined;
let seconds = 0;
if (parts.length === 1) {
seconds = lastNum;
} else if (parts.length === 2) {
seconds = parseInt(parts[0], 10) * 60 + lastNum;
} else if (parts.length === 3) {
seconds = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + lastNum;
} else {
return undefined;
}
return Math.round(seconds * 100);
}

const pad2 = (num: number): string => (num < 10 ? '0' : '') + num;

/** Format centis as H:MM:SS, MM:SS, or SS.t for display in input (tenths when non-integer). */
export function formatClockFromCentis(centis: number): string {
if (centis <= 0) return '0:00:00';
const date = new Date(centis * 10),
hours = Math.floor(centis / 360000),
mins = date.getUTCMinutes(),
secs = date.getUTCSeconds(),
tenths = Math.round((centis % 100) / 10);
const secStr = tenths > 0 ? `${pad2(secs)}.${tenths}` : pad2(secs);
if (hours > 0) {
return `${hours}:${pad2(mins)}:${secStr}`;
}
if (mins > 0) {
return `${mins}:${secStr}`;
}
return tenths > 0 ? `${secs}.${tenths}` : `${secs}`;
}
Loading
Loading