vimp-engine 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ export default class GamesCtrl {
|
|
|
29
29
|
vp.on('submit', 'submit', this);
|
|
30
30
|
vp.on('update-version', 'updateVersion', this);
|
|
31
31
|
vp.on('delete', 'remove', this);
|
|
32
|
+
vp.on('restore', 'restore', this);
|
|
32
33
|
vp.on('filter', 'filter', this);
|
|
33
34
|
vp.on('stage', 'stage', this);
|
|
34
35
|
vp.on('approve', 'approve', this);
|
|
@@ -81,6 +82,10 @@ export default class GamesCtrl {
|
|
|
81
82
|
this._model.remove(id, scope);
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
restore({ id }) {
|
|
86
|
+
this._model.restore(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
filter(status) {
|
|
85
90
|
this._model.setFilter(status);
|
|
86
91
|
}
|
|
@@ -156,7 +156,11 @@ export default class GamesModel {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// удаление игры. scope решает, чей список перечитать: карточка автора и
|
|
159
|
-
// очередь модерации показывают разные проекции одной
|
|
159
|
+
// очередь модерации показывают разные проекции одной строки.
|
|
160
|
+
//
|
|
161
|
+
// Удаление обратимо: игра уходит в графу Deleted очереди модерации и
|
|
162
|
+
// возвращается оттуда `restore` — подтверждения второй кнопкой оно
|
|
163
|
+
// поэтому не требует
|
|
160
164
|
async remove(id, scope = 'mine') {
|
|
161
165
|
const { ok, json } = await this._request(this._config.urls.remove(id), {
|
|
162
166
|
method: 'DELETE',
|
|
@@ -169,12 +173,34 @@ export default class GamesModel {
|
|
|
169
173
|
|
|
170
174
|
if (scope === 'admin') {
|
|
171
175
|
await this.loadAdmin();
|
|
176
|
+
|
|
177
|
+
// после loadAdmin, а не до: renderAdmin чистит строку панели (см.
|
|
178
|
+
// moderate). Снятая с раздачи последняя игра оставляет лобби без
|
|
179
|
+
// каталога, и сказать об этом обязан тот же тик
|
|
180
|
+
if (json?.warning) {
|
|
181
|
+
this.publisher.emit('warning', { scope: 'admin', code: json.warning });
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
return;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
await this.loadMine();
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
// возврат мягко удалённой игры из графы Deleted (только админ)
|
|
191
|
+
async restore(id) {
|
|
192
|
+
const { ok, json } = await this._request(this._config.urls.restore(id), {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!ok) {
|
|
197
|
+
this._fail('admin', json);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.loadAdmin();
|
|
202
|
+
}
|
|
203
|
+
|
|
178
204
|
// «Test»: мастер качает версию и кладёт её в каталог не раздаваемой.
|
|
179
205
|
// Манифест едет наружу — по нему вкладка админа поднимает комнату
|
|
180
206
|
async stage(id, version) {
|
|
@@ -218,9 +244,17 @@ export default class GamesModel {
|
|
|
218
244
|
await this.moderate(id, { authorNick: nick === '' ? null : nick });
|
|
219
245
|
}
|
|
220
246
|
|
|
247
|
+
// графа, в которой стоит игра. Мягко удалённая уходит в свою графу
|
|
248
|
+
// ЦЕЛИКОМ, сохраняя прежний статус: восстановление возвращает её туда,
|
|
249
|
+
// где она была, а показывать её ещё и в Pending/Published значило бы
|
|
250
|
+
// предлагать модерировать то, чего у игроков уже нет
|
|
251
|
+
_bucketOf(game) {
|
|
252
|
+
return game.deletedAt ? 'deleted' : game.status;
|
|
253
|
+
}
|
|
254
|
+
|
|
221
255
|
_emitAdmin() {
|
|
222
256
|
this.publisher.emit('admin-changed', {
|
|
223
|
-
games: this._all.filter(game => game
|
|
257
|
+
games: this._all.filter(game => this._bucketOf(game) === this._filter),
|
|
224
258
|
filter: this._filter,
|
|
225
259
|
versions: this._versions,
|
|
226
260
|
});
|
|
@@ -288,8 +288,10 @@ export default class GamesView {
|
|
|
288
288
|
item.appendChild(version);
|
|
289
289
|
item.appendChild(send);
|
|
290
290
|
item.appendChild(
|
|
291
|
-
this.
|
|
292
|
-
|
|
291
|
+
this._button(
|
|
292
|
+
'Delete',
|
|
293
|
+
() => this.publisher.emit('delete', { id: game.id, scope: 'mine' }),
|
|
294
|
+
'games-delete-btn',
|
|
293
295
|
),
|
|
294
296
|
);
|
|
295
297
|
this._mineList.appendChild(item);
|
|
@@ -337,6 +339,21 @@ export default class GamesView {
|
|
|
337
339
|
'games-item-title',
|
|
338
340
|
),
|
|
339
341
|
);
|
|
342
|
+
|
|
343
|
+
// Мягко удалённая игра модерации не подлежит: у игроков её уже нет, а
|
|
344
|
+
// решение по ней приняли. Из всей карточки ей нужны срок полного
|
|
345
|
+
// удаления и возврат — остальные кнопки правили бы то, чего не видно
|
|
346
|
+
if (game.deletedAt) {
|
|
347
|
+
item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
|
|
348
|
+
this._appendRepo(item, game.repoUrl);
|
|
349
|
+
item.appendChild(this._line(this._statusLine(game), 'games-purge-line'));
|
|
350
|
+
item.appendChild(
|
|
351
|
+
this._button('Restore', () => this.publisher.emit('restore', { id: game.id })),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return item;
|
|
355
|
+
}
|
|
356
|
+
|
|
340
357
|
// у игр, засеянных миграцией, автора нет вовсе: без запасного прочерка
|
|
341
358
|
// в строке печаталось бы литеральное "null"
|
|
342
359
|
item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
|
|
@@ -408,7 +425,11 @@ export default class GamesView {
|
|
|
408
425
|
),
|
|
409
426
|
);
|
|
410
427
|
item.appendChild(
|
|
411
|
-
this.
|
|
428
|
+
this._button(
|
|
429
|
+
'Delete',
|
|
430
|
+
() => this.publisher.emit('delete', { id: game.id, scope: 'admin' }),
|
|
431
|
+
'games-delete-btn',
|
|
432
|
+
),
|
|
412
433
|
);
|
|
413
434
|
|
|
414
435
|
return item;
|
|
@@ -438,6 +459,16 @@ export default class GamesView {
|
|
|
438
459
|
const status = STATUS_TITLES[game.status] ?? game.status;
|
|
439
460
|
const date = game.createdAt ? new Date(game.createdAt).toLocaleDateString() : '';
|
|
440
461
|
|
|
462
|
+
// у удалённой игры прежний статус — след того, куда её вернёт Restore, —
|
|
463
|
+
// а вести отсчёт человеку нужно от срока полного удаления: только он
|
|
464
|
+
// ограничен во времени
|
|
465
|
+
if (game.deletedAt) {
|
|
466
|
+
const deleted = new Date(game.deletedAt).toLocaleDateString();
|
|
467
|
+
const purge = game.purgeAt ? new Date(game.purgeAt).toLocaleDateString() : '—';
|
|
468
|
+
|
|
469
|
+
return `Deleted ${deleted} (was ${status}); removed for good on ${purge}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
441
472
|
return date ? `Status: ${status} (submitted ${date})` : `Status: ${status}`;
|
|
442
473
|
}
|
|
443
474
|
|
|
@@ -459,34 +490,17 @@ export default class GamesView {
|
|
|
459
490
|
});
|
|
460
491
|
}
|
|
461
492
|
|
|
462
|
-
|
|
463
|
-
// второго нажатия: первое переводит её в «Confirm delete», второе публикует
|
|
464
|
-
// событие. Повторная отрисовка списка возвращает её в исходное состояние —
|
|
465
|
-
// незавершённое подтверждение не переживает перерисовку
|
|
466
|
-
_deleteButton(onConfirm) {
|
|
467
|
-
const btn = this._button('Delete', () => {
|
|
468
|
-
if (btn.dataset.armed !== '1') {
|
|
469
|
-
btn.dataset.armed = '1';
|
|
470
|
-
btn.value = 'Confirm delete';
|
|
471
|
-
btn.classList.add('games-delete-armed');
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
onConfirm();
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
btn.className = 'games-delete-btn';
|
|
479
|
-
|
|
480
|
-
return btn;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
_button(value, onclick) {
|
|
493
|
+
_button(value, onclick, className) {
|
|
484
494
|
const btn = document.createElement('input');
|
|
485
495
|
|
|
486
496
|
btn.type = 'button';
|
|
487
497
|
btn.value = value;
|
|
488
498
|
btn.onclick = onclick;
|
|
489
499
|
|
|
500
|
+
if (className) {
|
|
501
|
+
btn.className = className;
|
|
502
|
+
}
|
|
503
|
+
|
|
490
504
|
return btn;
|
|
491
505
|
}
|
|
492
506
|
|
package/src/client/style.css
CHANGED
|
@@ -217,9 +217,9 @@ body.hide-cursor {
|
|
|
217
217
|
background: rgba(25, 25, 25, 0.9);
|
|
218
218
|
border-radius: 5px;
|
|
219
219
|
border: 3px solid #fff;
|
|
220
|
-
overflow:
|
|
220
|
+
overflow: auto;
|
|
221
221
|
width: 90%;
|
|
222
|
-
max-width:
|
|
222
|
+
max-width: 500px;
|
|
223
223
|
font-size: 10px;
|
|
224
224
|
line-height: 12px;
|
|
225
225
|
color: #fff;
|
|
@@ -438,7 +438,7 @@ body.hide-cursor {
|
|
|
438
438
|
background-color: #333;
|
|
439
439
|
border: 1px solid #eee;
|
|
440
440
|
padding: 10px;
|
|
441
|
-
margin:
|
|
441
|
+
margin: 5px auto 0;
|
|
442
442
|
text-shadow: 1px 1px 1px #000;
|
|
443
443
|
font-size: 12px;
|
|
444
444
|
line-height: 15px;
|
|
@@ -549,15 +549,11 @@ body.hide-cursor {
|
|
|
549
549
|
padding: 0 10px;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
/* ширина — контентная область правой панели лобби (её ширина минус
|
|
553
|
-
собственные поля .lobby-browser-inner), под которой футер и стоит;
|
|
554
|
-
max-width, а не width: в одну колонку он обязан ужиматься по экрану */
|
|
555
552
|
#lobby-footer {
|
|
556
|
-
width:
|
|
557
|
-
max-width: calc(var(--lobby-browser-width) - 2 * var(--lobby-panel-pad));
|
|
553
|
+
width: calc(var(--lobby-width) + var(--lobby-panel-pad));
|
|
558
554
|
font-size: 10px;
|
|
559
555
|
padding: 0;
|
|
560
|
-
margin: 0 var(--lobby-panel-pad) 0 0;
|
|
556
|
+
margin: 0 calc(var(--lobby-panel-pad) / 2) 0 0;
|
|
561
557
|
}
|
|
562
558
|
|
|
563
559
|
/* три равные ячейки: середину держит не space-between, а сама равность —
|
|
@@ -670,11 +666,7 @@ body.hide-cursor {
|
|
|
670
666
|
/*lobby*/
|
|
671
667
|
|
|
672
668
|
#lobby {
|
|
673
|
-
|
|
674
|
-
(секция auth выше), поэтому числа живут в одном месте — иначе смена
|
|
675
|
-
ширины любой панели молча разъезжает вёрстку */
|
|
676
|
-
--lobby-setup-width: 410px;
|
|
677
|
-
--lobby-browser-width: 430px;
|
|
669
|
+
--lobby-width: 370px;
|
|
678
670
|
--lobby-panel-pad: 10px;
|
|
679
671
|
|
|
680
672
|
background-color: #222;
|
|
@@ -832,12 +824,12 @@ body.hide-cursor {
|
|
|
832
824
|
высоты более высокой панели */
|
|
833
825
|
.lobby-grid {
|
|
834
826
|
display: flex;
|
|
835
|
-
gap:
|
|
827
|
+
gap: 5px;
|
|
836
828
|
justify-content: center;
|
|
837
829
|
}
|
|
838
830
|
|
|
839
831
|
#lobby-setup-panel {
|
|
840
|
-
width: var(--lobby-
|
|
832
|
+
width: var(--lobby-width);
|
|
841
833
|
}
|
|
842
834
|
|
|
843
835
|
/* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
|
|
@@ -846,9 +838,8 @@ body.hide-cursor {
|
|
|
846
838
|
вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
|
|
847
839
|
расчёте высоты строки) */
|
|
848
840
|
#lobby-browser-panel {
|
|
849
|
-
width: var(--lobby-
|
|
841
|
+
width: var(--lobby-width);
|
|
850
842
|
position: relative;
|
|
851
|
-
padding: 0;
|
|
852
843
|
}
|
|
853
844
|
|
|
854
845
|
.lobby-browser-inner {
|
|
@@ -1147,16 +1138,16 @@ body.hide-cursor {
|
|
|
1147
1138
|
color: #eee;
|
|
1148
1139
|
}
|
|
1149
1140
|
|
|
1150
|
-
/* удаление
|
|
1151
|
-
|
|
1141
|
+
/* удаление обратимо (игра уходит в графу Deleted на 30 суток), но выделяется
|
|
1142
|
+
среди соседних кнопок: рядом стоит правка версии, а не действие того же веса */
|
|
1152
1143
|
.games-delete-btn {
|
|
1153
1144
|
color: #d66;
|
|
1154
1145
|
}
|
|
1155
1146
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
color: #
|
|
1147
|
+
/* срок полного удаления — единственное, что в карточке удалённой игры
|
|
1148
|
+
ограничено во времени */
|
|
1149
|
+
.games-purge-line {
|
|
1150
|
+
color: #d66;
|
|
1160
1151
|
}
|
|
1161
1152
|
|
|
1162
1153
|
/* кнопки входа в панель стоят рядом с Sign out в бейдже пользователя */
|
package/src/config/lobby.js
CHANGED
|
@@ -218,19 +218,26 @@ export default {
|
|
|
218
218
|
staged: '/admin/games/manifest.json',
|
|
219
219
|
// «Test»: скачать версию и положить её в каталог не раздаваемой
|
|
220
220
|
stage: id => `/admin/games/${encodeURIComponent(id)}/stage`,
|
|
221
|
+
// возврат мягко удалённой игры из графы Deleted (только админ)
|
|
222
|
+
restore: id => `/admin/games/${encodeURIComponent(id)}/restore`,
|
|
221
223
|
// решение модератора
|
|
222
224
|
moderate: id => `/admin/games/${encodeURIComponent(id)}`,
|
|
223
225
|
// что опубликовано в npm — индикатор «есть версия новее»
|
|
224
226
|
versions: id => `/admin/games/${encodeURIComponent(id)}/versions`,
|
|
225
227
|
},
|
|
226
228
|
|
|
227
|
-
// фильтры очереди модерации: id
|
|
228
|
-
//
|
|
229
|
+
// фильтры («графы») очереди модерации: id -> подпись кнопки. Первые
|
|
230
|
+
// четыре — статусы реестра, и значения обязаны совпадать со статусами
|
|
231
|
+
// auth-сервиса. Пятая графа стоит особняком: 'deleted' статусом НЕ
|
|
232
|
+
// является (мягко удалённая игра сохраняет свой прежний статус, иначе
|
|
233
|
+
// восстанавливать было бы не во что) — под неё попадает всё с
|
|
234
|
+
// непустым deletedAt, см. GamesModel._bucketOf
|
|
229
235
|
statuses: [
|
|
230
236
|
{ id: 'pending', title: 'Pending' },
|
|
231
237
|
{ id: 'approved', title: 'Published' },
|
|
232
238
|
{ id: 'rejected', title: 'Rejected' },
|
|
233
239
|
{ id: 'disabled', title: 'Disabled' },
|
|
240
|
+
{ id: 'deleted', title: 'Deleted' },
|
|
234
241
|
],
|
|
235
242
|
defaultStatus: 'pending',
|
|
236
243
|
|