diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index fcce11d39e72..451fa820f4b6 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -88,7 +88,7 @@ interface QuestionOpts { type SoundMoveOpts = { name?: string; // either provide this or valid san/uci - san?: string; + lazySan: () => string | undefined; uci?: string; volume?: number; }; @@ -110,9 +110,9 @@ interface SoundI { setVolume(v: number): void; speech(v?: boolean): boolean; changeSet(s: string): void; - say(text: string, cut?: boolean, force?: boolean, translated?: boolean): boolean; - saySan(san?: San, cut?: boolean, force?: boolean): void; - sayOrPlay(name: string, text: string): void; + say(lazyText: () => string, cut?: boolean, force?: boolean, translated?: boolean): boolean; + saySan(lazySan: () => San | undefined, cut?: boolean, force?: boolean): void; + sayOrPlay(name: string, lazyText: () => string): void; preloadBoardSounds(): void; url(name: string): string; } diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 55bdc2eaafa3..91012d6c0fae 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -402,11 +402,15 @@ export default class AnalyseCtrl { this.setPath(path); if (pathChanged) { if (this.study) this.study.setPath(path, this.node); - if (isForwardStep) site.sound.move(this.node); + if (isForwardStep) + site.sound.move({ + uci: this.node.uci, + lazySan: () => this.node.san, + }); this.threatMode(false); this.ceval?.stop(); this.startCeval(); - site.sound.saySan(this.node.san, true); + site.sound.saySan(() => this.node.san, true); } this.justPlayed = this.justDropped = this.justCaptured = undefined; this.explorer.setNode(); diff --git a/ui/dasher/src/sound.ts b/ui/dasher/src/sound.ts index bb5015fc9a1c..7699b7f44d88 100644 --- a/ui/dasher/src/sound.ts +++ b/ui/dasher/src/sound.ts @@ -95,7 +95,7 @@ export class SoundCtrl extends PaneCtrl { if (site.sound.speech()) { site.sound.changeSet('standard'); this.postSet('standard'); - site.sound.say('Speech synthesis ready'); + site.sound.say(() => 'Speech synthesis ready'); } else { site.sound.changeSet(k); site.sound.play('genericNotify'); @@ -107,6 +107,6 @@ export class SoundCtrl extends PaneCtrl { private volume = (v: number) => { site.sound.setVolume(v); // plays a move sound if speech is off - site.sound.sayOrPlay('move', 'knight F 7'); + site.sound.sayOrPlay('move', () => 'knight F 7'); }; } diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 57357062870b..5fff2db46aff 100755 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -364,7 +364,7 @@ export default class PuzzleCtrl implements ParentCtrl { const progress = moveTest(this); this.setAutoShapes(); - if (progress === 'fail') site.sound.say('incorrect'); + if (progress === 'fail') site.sound.say(() => 'incorrect'); if (progress) this.applyProgress(progress); this.reorderChildren(path); this.redraw(); @@ -457,7 +457,7 @@ export default class PuzzleCtrl implements ParentCtrl { this.round = res.round; if (res.round?.ratingDiff) this.session.setRatingDiff(this.data.puzzle.id, res.round.ratingDiff); } - if (win) site.sound.say('Success!'); + if (win) site.sound.say(() => 'Success!'); if (next) { this.next.resolve(this.data.replay && res.replayComplete ? this.data.replay : next); if (this.streak && win) this.streak.onComplete(true, res.next); @@ -562,8 +562,11 @@ export default class PuzzleCtrl implements ParentCtrl { this.withGround(this.showGround); if (pathChanged) { if (isForwardStep) { - site.sound.saySan(this.node.san); - site.sound.move(this.node); + site.sound.saySan(() => this.node.san); + site.sound.move({ + uci: this.node.uci, + lazySan: () => this.node.san, + }); } this.threatMode(false); this.ceval.stop(); diff --git a/ui/round/src/clock/clockCtrl.ts b/ui/round/src/clock/clockCtrl.ts index 9439d98841cd..409dc5c413c4 100644 --- a/ui/round/src/clock/clockCtrl.ts +++ b/ui/round/src/clock/clockCtrl.ts @@ -170,22 +170,22 @@ export class ClockController { isRunning = (): boolean => this.times.activeColor !== undefined; - speak = (): void => { - const msgs = ['white', 'black'].map(color => { - const time = this.millisOf(color as Color); - const date = new Date(time); - const msg = - (time >= 3600000 ? simplePlural(Math.floor(time / 3600000), 'hour') : '') + - ' ' + - simplePlural(date.getUTCMinutes(), 'minute') + - ' ' + - simplePlural(date.getUTCSeconds(), 'second'); - return `${color} ${msg}`; - }); - site.sound.say(msgs.join('. ')); - }; + speak = (): boolean => + site.sound.say(() => + ['white', 'black'] + .map(color => { + const time = this.millisOf(color as Color); + const date = new Date(time); + const msg = + (time >= 3600000 ? naivePlural(Math.floor(time / 3600000), 'hour') : '') + + ' ' + + naivePlural(date.getUTCMinutes(), 'minute') + + ' ' + + naivePlural(date.getUTCSeconds(), 'second'); + return `${color} ${msg}`; + }) + .join('. '), + ); } -function simplePlural(nb: number, word: string) { - return `${nb} ${word}${nb !== 1 ? 's' : ''}`; -} +const naivePlural = (nb: number, word: string) => `${nb} ${word}${nb !== 1 ? 's' : ''}`; diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 5da7a1c1d418..20a18f4fddd6 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -174,11 +174,13 @@ export default class RoundController implements MoveRootCtrl { atomic.capture(this, dest); return; } - const fen = this.ply === 0 ? this.data.game.fen : this.stepAt(this.ply - 1).fen; - const san = sanOf(readFen(fen), orig + dest); + const lazySan = () => { + const fen = this.ply === 0 ? this.data.game.fen : this.stepAt(this.ply - 1).fen; + return sanOf(readFen(fen), orig + dest); + }; - site.sound.move({ san, uci: orig + dest }); - site.sound.saySan(san); + site.sound.move({ lazySan, uci: orig + dest }); + site.sound.saySan(lazySan); }; private startPromotion = (orig: Key, dest: Key, meta: MoveMetadata) => @@ -233,7 +235,7 @@ export default class RoundController implements MoveRootCtrl { userJump = (ply: Ply): void => { this.toSubmit = undefined; this.chessground.selectSquare(null); - if (ply != this.ply && this.jump(ply)) site.sound.saySan(this.stepAt(this.ply).san, true); + if (ply != this.ply && this.jump(ply)) site.sound.saySan(() => this.stepAt(this.ply).san, true); else this.redraw(); }; @@ -262,7 +264,11 @@ export default class RoundController implements MoveRootCtrl { }; this.chessground.cancelPremove(); this.chessground.set(config); - if (s.san && isForwardStep) site.sound.move(s); + if (s.san && isForwardStep) + site.sound.move({ + uci: s.uci, + lazySan: () => s.san, + }); this.autoScroll(); this.pluginUpdate(s.fen); pubsub.emit('ply', ply); @@ -346,7 +352,7 @@ export default class RoundController implements MoveRootCtrl { if (!meta.preConfirmed && this.confirmMoveToggle() && !meta.premove) { if (site.sound.speech()) { - const spoken = `${speakable(sanOf(readFen(this.stepAt(this.ply).fen), move.u))}. confirm?`; + const spoken = () => `${speakable(sanOf(readFen(this.stepAt(this.ply).fen), move.u))}. confirm?`; site.sound.say(spoken, false, true); } this.toSubmit = move; @@ -582,8 +588,8 @@ export default class RoundController implements MoveRootCtrl { this.onChange(); if (d.tv) setTimeout(site.reload, 10000); wakeLock.release(); - if (this.data.game.status.name === 'started') site.sound.saySan(this.stepAt(this.ply).san, false); - else site.sound.say(viewStatus(this), false, false, true); + if (this.data.game.status.name === 'started') site.sound.saySan(() => this.stepAt(this.ply).san, false); + else site.sound.say(() => viewStatus(this), false, false, true); }; challengeRematch = async (): Promise => { diff --git a/ui/site/src/sound.ts b/ui/site/src/sound.ts index 028b4b38c225..a94a84e33b9f 100644 --- a/ui/site/src/sound.ts +++ b/ui/site/src/sound.ts @@ -136,11 +136,12 @@ export default new (class implements SoundI { return this.speechStorage.get(); }; - say = (text: string, cut = false, force = false, translated = false) => { + say = (lazyText: () => string, cut = false, force = false, translated = false) => { if (typeof window.speechSynthesis === 'undefined') return false; try { if (cut) speechSynthesis.cancel(); if (!this.speech() && !force) return false; + const text = lazyText(); const msg = new SpeechSynthesisUtterance(text); msg.volume = this.getVolume(); msg.lang = translated ? document.documentElement.lang : 'en-US'; @@ -157,9 +158,10 @@ export default new (class implements SoundI { } }; - saySan = (san?: San, cut?: boolean, force?: boolean) => this.say(speakable(san), cut, force); + saySan = (lazySan: () => San | undefined, cut?: boolean, force?: boolean) => + this.say(() => speakable(lazySan()), cut, force); - sayOrPlay = (name: string, text: string) => this.say(text) || this.play(name); + sayOrPlay = (name: string, lazyText: () => string) => this.say(lazyText) || this.play(name); changeSet = (s: string) => { if (isIos()) this.ctx?.resume();