personal opening explorer UI WIP
parent
ca36d40fce
commit
b1a398b66d
|
@ -94,6 +94,7 @@ final class Env(
|
|||
) {
|
||||
|
||||
val explorerEndpoint = config.get[String]("explorer.endpoint")
|
||||
val explorer3Endpoint = config.get[String]("explorer.endpoint3")
|
||||
val tablebaseEndpoint = config.get[String]("explorer.tablebase.endpoint")
|
||||
|
||||
val appVersionDate = config.getOptional[String]("app.version.date")
|
||||
|
|
|
@ -84,7 +84,8 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
|
|||
}
|
||||
ContentSecurityPolicy(
|
||||
defaultSrc = List("'self'", assets),
|
||||
connectSrc = "'self'" :: assets :: sockets ::: env.explorerEndpoint :: env.tablebaseEndpoint :: Nil,
|
||||
connectSrc =
|
||||
"'self'" :: assets :: sockets ::: env.explorerEndpoint :: env.tablebaseEndpoint :: env.explorer3Endpoint :: Nil,
|
||||
styleSrc = List("'self'", "'unsafe-inline'", assets),
|
||||
frameSrc = List("'self'", assets, "https://www.youtube.com", "https://player.twitch.tv"),
|
||||
workerSrc = List("'self'", assets),
|
||||
|
|
|
@ -40,6 +40,7 @@ object Environment
|
|||
def apiVersion = lila.api.Mobile.Api.currentVersion
|
||||
|
||||
def explorerEndpoint = env.explorerEndpoint
|
||||
def explorer3Endpoint = env.explorer3Endpoint
|
||||
def tablebaseEndpoint = env.tablebaseEndpoint
|
||||
|
||||
def isChatPanicEnabled = env.chat.panic.enabled
|
||||
|
|
|
@ -94,14 +94,11 @@ object replay {
|
|||
embedJsUnsafeLoadThen(s"""LichessAnalyse.boot(${safeJsonValue(
|
||||
Json
|
||||
.obj(
|
||||
"data" -> data,
|
||||
"i18n" -> jsI18n(),
|
||||
"userId" -> ctx.userId,
|
||||
"chat" -> chatJson,
|
||||
"explorer" -> Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
)
|
||||
"data" -> data,
|
||||
"i18n" -> jsI18n(),
|
||||
"userId" -> ctx.userId,
|
||||
"chat" -> chatJson,
|
||||
"explorer" -> views.html.board.bits.explorerEndpoints
|
||||
)
|
||||
.add("hunter" -> isGranted(_.Hunter))
|
||||
)})""")
|
||||
|
|
|
@ -42,6 +42,12 @@ object bits {
|
|||
)
|
||||
.add("fen" -> fen)
|
||||
|
||||
val explorerEndpoints = Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"endpoint3" -> explorer3Endpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
)
|
||||
|
||||
private val i18nKeyes = List(
|
||||
trans.setTheBoard,
|
||||
trans.boardEditor,
|
||||
|
|
|
@ -28,13 +28,10 @@ object userAnalysis {
|
|||
analyseNvuiTag,
|
||||
embedJsUnsafe(s"""lichess.userAnalysis=${safeJsonValue(
|
||||
Json.obj(
|
||||
"data" -> data,
|
||||
"i18n" -> userAnalysisI18n(withForecast = withForecast),
|
||||
"explorer" -> Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
),
|
||||
"wiki" -> pov.game.variant.standard
|
||||
"data" -> data,
|
||||
"i18n" -> userAnalysisI18n(withForecast = withForecast),
|
||||
"explorer" -> bits.explorerEndpoints,
|
||||
"wiki" -> pov.game.variant.standard
|
||||
)
|
||||
)}""")
|
||||
),
|
||||
|
|
|
@ -165,7 +165,8 @@ object userAnalysisI18n {
|
|||
trans.maybeIncludeMoreGamesFromThePreferencesMenu,
|
||||
trans.winPreventedBy50MoveRule,
|
||||
trans.lossSavedBy50MoveRule,
|
||||
trans.allSet
|
||||
trans.allSet,
|
||||
trans.study.searchByUsername
|
||||
).map(_.key)
|
||||
|
||||
private val forecastTranslations: Vector[MessageKey] = Vector(
|
||||
|
|
|
@ -26,10 +26,7 @@ object show {
|
|||
"study" -> data.study,
|
||||
"data" -> data.analysis,
|
||||
"i18n" -> board.userAnalysisI18n(),
|
||||
"explorer" -> Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
)
|
||||
"explorer" -> views.html.board.bits.explorerEndpoints
|
||||
)
|
||||
)}""")
|
||||
),
|
||||
|
|
|
@ -44,10 +44,7 @@ object show {
|
|||
localMod = ctx.userId.??(rt.study.canContribute)
|
||||
)
|
||||
),
|
||||
"explorer" -> Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
),
|
||||
"explorer" -> views.html.board.bits.explorerEndpoints,
|
||||
"socketUrl" -> views.html.study.show.socketUrl(rt.study.id.value),
|
||||
"socketVersion" -> socketVersion.value
|
||||
)
|
||||
|
|
|
@ -43,10 +43,7 @@ object show {
|
|||
localMod = ctx.userId exists s.canContribute
|
||||
)
|
||||
},
|
||||
"explorer" -> Json.obj(
|
||||
"endpoint" -> explorerEndpoint,
|
||||
"tablebaseEndpoint" -> tablebaseEndpoint
|
||||
),
|
||||
"explorer" -> views.html.board.bits.explorerEndpoints,
|
||||
"socketUrl" -> socketUrl(s.id.value),
|
||||
"socketVersion" -> socketVersion.value
|
||||
)
|
||||
|
|
|
@ -324,6 +324,7 @@ streamer {
|
|||
}
|
||||
explorer {
|
||||
endpoint = "https://explorer.lichess.ovh"
|
||||
endpoint3 = "https://explorer.lichess.ovh"
|
||||
internal_endpoint = "http://explorer.lichess.ovh"
|
||||
tablebase = {
|
||||
endpoint = "https://tablebase.lichess.ovh"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@import 'layout';
|
||||
@import 'tools';
|
||||
@import 'action-menu';
|
||||
@import 'explorer';
|
||||
@import 'explorer/explorer';
|
||||
@import 'training';
|
||||
@import 'practice';
|
||||
@import 'fork';
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
.explorer__config {
|
||||
section {
|
||||
margin: 0.4em $block-gap 0 $block-gap;
|
||||
}
|
||||
|
||||
section.save {
|
||||
text-align: center;
|
||||
padding: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
@extend %metal;
|
||||
flex-grow: 1;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
@include transition;
|
||||
|
||||
border: $border;
|
||||
border-width: 1px 0 1px 1px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:first-child {
|
||||
@extend %box-radius-left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@extend %box-radius-right;
|
||||
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend %metal-hover;
|
||||
}
|
||||
|
||||
&[aria-pressed='true'] {
|
||||
background: $c-secondary;
|
||||
color: $c-secondary-over;
|
||||
text-shadow: 1px 0 0 rgba(0, 0, 0, 0.5);
|
||||
font-weight: bold;
|
||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@extend %flex-center;
|
||||
margin-top: 1em;
|
||||
.user-link {
|
||||
@extend %box-radius;
|
||||
font-size: 1.3em;
|
||||
padding-right: 1em;
|
||||
|
||||
background: mix($c-primary, $c-bg-box, 12%);
|
||||
padding: 0.2em 0.6em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__player__choice {
|
||||
> div {
|
||||
// for user complete
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
// for user complete
|
||||
overflow: visible !important;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
padding-top: 2em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@import './config';
|
||||
|
||||
.explorer-box {
|
||||
position: relative;
|
||||
flex: 2.5 1 0px;
|
||||
|
@ -291,61 +293,4 @@
|
|||
font-size: 40px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.config {
|
||||
section {
|
||||
margin: 0.4em $block-gap 0 $block-gap;
|
||||
}
|
||||
|
||||
section.save {
|
||||
text-align: center;
|
||||
padding: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
@extend %metal;
|
||||
flex-grow: 1;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
@include transition;
|
||||
|
||||
border: $border;
|
||||
border-width: 1px 0 1px 1px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:first-child {
|
||||
@extend %box-radius-left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@extend %box-radius-right;
|
||||
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend %metal-hover;
|
||||
}
|
||||
|
||||
&[aria-pressed='true'] {
|
||||
background: $c-secondary;
|
||||
color: $c-secondary-over;
|
||||
text-shadow: 1px 0 0 rgba(0, 0, 0, 0.5);
|
||||
font-weight: bold;
|
||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { h, VNode } from 'snabbdom';
|
||||
import { prop } from 'common';
|
||||
import { bind, dataIcon } from 'common/snabbdom';
|
||||
import { bind, dataIcon, onInsert } from 'common/snabbdom';
|
||||
import { storedProp, storedJsonProp, StoredJsonProp } from 'common/storage';
|
||||
import { Game } from '../interfaces';
|
||||
import { ExplorerDb, ExplorerSpeed, ExplorerConfigData, ExplorerConfigCtrl } from './interfaces';
|
||||
import { snabModal } from 'common/modal';
|
||||
|
||||
const allSpeeds: ExplorerSpeed[] = ['bullet', 'blitz', 'rapid', 'classical'];
|
||||
const allRatings = [1600, 1800, 2000, 2200, 2500];
|
||||
|
@ -11,11 +12,11 @@ const allRatings = [1600, 1800, 2000, 2200, 2500];
|
|||
export function controller(game: Game, onClose: () => void, trans: Trans, redraw: () => void): ExplorerConfigCtrl {
|
||||
const variant = game.variant.key === 'fromPosition' ? 'standard' : game.variant.key;
|
||||
|
||||
const available: ExplorerDb[] = ['lichess'];
|
||||
const available: ExplorerDb[] = ['lichess', 'player'];
|
||||
if (variant === 'standard') available.unshift('masters');
|
||||
|
||||
const data: ExplorerConfigData = {
|
||||
open: prop(false),
|
||||
open: prop(true),
|
||||
db: {
|
||||
available,
|
||||
selected:
|
||||
|
@ -33,6 +34,10 @@ export function controller(game: Game, onClose: () => void, trans: Trans, redraw
|
|||
available: allSpeeds,
|
||||
selected: storedJsonProp<ExplorerSpeed[]>('explorer.speed', () => allSpeeds),
|
||||
},
|
||||
playerName: {
|
||||
open: prop(false),
|
||||
value: storedProp<string | undefined>('explorer.player.name', document.body.dataset['user'] || ''),
|
||||
},
|
||||
};
|
||||
|
||||
const toggleMany = function <T>(c: StoredJsonProp<T[]>, value: T) {
|
||||
|
@ -68,69 +73,30 @@ export function controller(game: Game, onClose: () => void, trans: Trans, redraw
|
|||
}
|
||||
|
||||
export function view(ctrl: ExplorerConfigCtrl): VNode[] {
|
||||
const d = ctrl.data;
|
||||
return [
|
||||
h('section.db', [
|
||||
h('label', ctrl.trans.noarg('database')),
|
||||
h(
|
||||
'div.choices',
|
||||
d.db.available.map(function (s) {
|
||||
return h(
|
||||
ctrl.data.db.available.map(s =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
attrs: {
|
||||
'aria-pressed': `${d.db.selected() === s}`,
|
||||
'aria-pressed': `${ctrl.data.db.selected() === s}`,
|
||||
},
|
||||
hook: bind('click', _ => ctrl.toggleDb(s), ctrl.redraw),
|
||||
},
|
||||
s
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
d.db.selected() === 'masters'
|
||||
? h('div.masters.message', [
|
||||
h('i', { attrs: dataIcon('') }),
|
||||
h('p', ctrl.trans('masterDbExplanation', 2200, '1952', '2019')),
|
||||
])
|
||||
: h('div', [
|
||||
h('section.rating', [
|
||||
h('label', ctrl.trans.noarg('averageElo')),
|
||||
h(
|
||||
'div.choices',
|
||||
d.rating.available.map(function (r) {
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
attrs: {
|
||||
'aria-pressed': `${d.rating.selected().includes(r)}`,
|
||||
},
|
||||
hook: bind('click', _ => ctrl.toggleRating(r), ctrl.redraw),
|
||||
},
|
||||
r.toString()
|
||||
);
|
||||
})
|
||||
),
|
||||
]),
|
||||
h('section.speed', [
|
||||
h('label', ctrl.trans.noarg('timeControl')),
|
||||
h(
|
||||
'div.choices',
|
||||
d.speed.available.map(function (s) {
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
attrs: {
|
||||
'aria-pressed': `${d.speed.selected().includes(s)}`,
|
||||
},
|
||||
hook: bind('click', _ => ctrl.toggleSpeed(s), ctrl.redraw),
|
||||
},
|
||||
s
|
||||
);
|
||||
})
|
||||
),
|
||||
]),
|
||||
]),
|
||||
ctrl.data.db.selected() === 'masters'
|
||||
? masterDb(ctrl)
|
||||
: ctrl.data.db.selected() === 'lichess'
|
||||
? lichessDb(ctrl)
|
||||
: playerDb(ctrl),
|
||||
h(
|
||||
'section.save',
|
||||
h(
|
||||
|
@ -144,3 +110,108 @@ export function view(ctrl: ExplorerConfigCtrl): VNode[] {
|
|||
),
|
||||
];
|
||||
}
|
||||
|
||||
const playerDb = (ctrl: ExplorerConfigCtrl) => {
|
||||
const name = ctrl.data.playerName.value();
|
||||
return h('div.player-db', [
|
||||
ctrl.data.playerName.open() ? playerModal(ctrl) : undefined,
|
||||
h('section.name', [
|
||||
name
|
||||
? h(
|
||||
'span.user-link.ulpt',
|
||||
{
|
||||
hook: onInsert(lichess.powertip.manualUser),
|
||||
attrs: { 'data-href': `/@/${name}` },
|
||||
},
|
||||
name
|
||||
)
|
||||
: undefined,
|
||||
h(
|
||||
`button.${name ? 'button-link' : 'button'}`,
|
||||
{
|
||||
hook: bind('click', () => ctrl.data.playerName.open(true), ctrl.redraw),
|
||||
},
|
||||
name ? 'Change' : 'Select a Lichess player'
|
||||
),
|
||||
]),
|
||||
]);
|
||||
};
|
||||
|
||||
const playerModal = (ctrl: ExplorerConfigCtrl) => {
|
||||
return snabModal({
|
||||
class: 'explorer__config__player__choice',
|
||||
onClose() {
|
||||
ctrl.data.playerName.open(false);
|
||||
ctrl.redraw();
|
||||
},
|
||||
content: [
|
||||
h('h2', 'Personal opening explorer'),
|
||||
h('div.input-wrapper', [
|
||||
h('input', {
|
||||
attrs: { placeholder: ctrl.trans.noarg('searchByUsername') },
|
||||
hook: onInsert<HTMLInputElement>(input =>
|
||||
lichess.userComplete().then(uac => {
|
||||
uac({
|
||||
input,
|
||||
tag: 'span',
|
||||
onSelect(v) {
|
||||
// input.value = v.name;
|
||||
ctrl.data.playerName.value(v.name);
|
||||
ctrl.data.playerName.open(false);
|
||||
ctrl.redraw();
|
||||
},
|
||||
});
|
||||
input.focus();
|
||||
})
|
||||
),
|
||||
}),
|
||||
]),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const masterDb = (ctrl: ExplorerConfigCtrl) =>
|
||||
h('div.masters.message', [
|
||||
h('i', { attrs: dataIcon('') }),
|
||||
h('p', ctrl.trans('masterDbExplanation', 2200, '1952', '2019')),
|
||||
]);
|
||||
|
||||
const lichessDb = (ctrl: ExplorerConfigCtrl) =>
|
||||
h('div', [
|
||||
h('section.rating', [
|
||||
h('label', ctrl.trans.noarg('averageElo')),
|
||||
h(
|
||||
'div.choices',
|
||||
ctrl.data.rating.available.map(r =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
attrs: {
|
||||
'aria-pressed': `${ctrl.data.rating.selected().includes(r)}`,
|
||||
},
|
||||
hook: bind('click', _ => ctrl.toggleRating(r), ctrl.redraw),
|
||||
},
|
||||
r.toString()
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
h('section.speed', [
|
||||
h('label', ctrl.trans.noarg('timeControl')),
|
||||
h(
|
||||
'div.choices',
|
||||
ctrl.data.speed.available.map(s =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
attrs: {
|
||||
'aria-pressed': `${ctrl.data.speed.selected().includes(s)}`,
|
||||
},
|
||||
hook: bind('click', _ => ctrl.toggleSpeed(s), ctrl.redraw),
|
||||
},
|
||||
s
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
|
|
@ -78,7 +78,12 @@ export default function (root: AnalyseCtrl, opts: ExplorerOpts, allow: boolean):
|
|||
? xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen)
|
||||
: xhr.opening({
|
||||
endpoint: opts.endpoint,
|
||||
endpoint3: opts.endpoint3,
|
||||
db: config.data.db.selected() as ExplorerDb,
|
||||
personal: {
|
||||
player: config.data.playerName.value(),
|
||||
color: root.getOrientation(),
|
||||
},
|
||||
variant: effectiveVariant,
|
||||
rootFen: root.nodeList[0].fen,
|
||||
play: root.nodeList.slice(1).map(s => s.uci!),
|
||||
|
@ -180,6 +185,7 @@ export default function (root: AnalyseCtrl, opts: ExplorerOpts, allow: boolean):
|
|||
return xhr
|
||||
.opening({
|
||||
endpoint: opts.endpoint,
|
||||
endpoint3: opts.endpoint3,
|
||||
db: 'masters',
|
||||
rootFen: fen,
|
||||
play: [],
|
||||
|
|
|
@ -426,7 +426,7 @@ export default function (ctrl: AnalyseCtrl): VNode | undefined {
|
|||
{
|
||||
class: {
|
||||
loading,
|
||||
config: configOpened,
|
||||
explorer__config: configOpened,
|
||||
reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2),
|
||||
},
|
||||
hook: {
|
||||
|
|
|
@ -3,7 +3,12 @@ import * as xhr from 'common/xhr';
|
|||
|
||||
interface OpeningXhrOpts {
|
||||
endpoint: string;
|
||||
endpoint3: string;
|
||||
db: ExplorerDb;
|
||||
personal?: {
|
||||
player: string;
|
||||
color: Color;
|
||||
};
|
||||
rootFen: Fen;
|
||||
play: string[];
|
||||
fen: Fen;
|
||||
|
@ -14,7 +19,8 @@ interface OpeningXhrOpts {
|
|||
}
|
||||
|
||||
export function opening(opts: OpeningXhrOpts): Promise<OpeningData> {
|
||||
const url = new URL(opts.db === 'lichess' ? '/lichess' : '/master', opts.endpoint);
|
||||
const endpoint = opts.db == 'player' ? opts.endpoint3 : opts.endpoint;
|
||||
const url = new URL(opts.db === 'lichess' ? '/lichess' : opts.db == 'player' ? '/personal' : '/master', endpoint);
|
||||
const params = url.searchParams;
|
||||
params.set('fen', opts.rootFen);
|
||||
params.set('play', opts.play.join(','));
|
||||
|
@ -23,6 +29,11 @@ export function opening(opts: OpeningXhrOpts): Promise<OpeningData> {
|
|||
if (opts.speeds) for (const speed of opts.speeds) params.append('speeds[]', speed);
|
||||
if (opts.ratings) for (const rating of opts.ratings) params.append('ratings[]', rating.toString());
|
||||
}
|
||||
if (opts.db === 'player' && opts.personal) {
|
||||
params.set('player', opts.personal.player);
|
||||
params.set('color', opts.personal.color);
|
||||
// params.set('update', 'true');
|
||||
}
|
||||
if (!opts.withGames) {
|
||||
params.set('topGames', '0');
|
||||
params.set('recentGames', '0');
|
||||
|
|
|
@ -6,12 +6,17 @@ export interface Hovering {
|
|||
uci: Uci;
|
||||
}
|
||||
|
||||
export type ExplorerDb = 'lichess' | 'masters';
|
||||
export type ExplorerDb = 'lichess' | 'masters' | 'player';
|
||||
|
||||
export type ExplorerSpeed = 'bullet' | 'blitz' | 'rapid' | 'classical';
|
||||
|
||||
export interface PlayerOpts {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ExplorerOpts {
|
||||
endpoint: string;
|
||||
endpoint3: string;
|
||||
tablebaseEndpoint: string;
|
||||
}
|
||||
|
||||
|
@ -29,6 +34,10 @@ export interface ExplorerConfigData {
|
|||
available: ExplorerSpeed[];
|
||||
selected: StoredJsonProp<ExplorerSpeed[]>;
|
||||
};
|
||||
playerName: {
|
||||
open: Prop<boolean>;
|
||||
value: StoredProp<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExplorerConfigCtrl {
|
||||
|
|
Loading…
Reference in New Issue