This repository has been archived by the owner on Jul 4, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathelo.ts
1430 lines (1333 loc) · 44.8 KB
/
elo.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import * as fs from "fs"
import * as proc from "child_process"
import * as user from "./user.js"
import * as site from "./site.js"
import * as seltree from "./seltree.js"
import { remote } from "electron"
const { Menu, MenuItem } = remote
let showRoot = ""
let pinnedShow = {}
let pinnedGroup = {}
let pinnedUnit = {}
// currentNotificationID는 notify가 호출될 때마다 1씩 증가한다.
let currentNotificationID = 0
let selection = seltree.New()
// init은 elo를 초기화 한다.
// 실행은 모든 함수가 정의되고 난 마지막에 하게 된다.
function init() {
site.Init()
ensureDirExist(configDir())
loadPinnedShow()
loadPinnedGroup()
loadPinnedUnit()
ensureElementExist("show-box")
ensureElementExist("unit-box")
ensureElementExist("part-box")
ensureElementExist("task-box")
ensureElementExist("category-menu")
addCategoryMenuItems()
loadCategory()
reloadShows()
loadSelection()
uiEvent(function() {
// 원래 있던 항목들이 사라지는 경우 아래 함수는 에러가 난다.
// 이런 경우에 elo가 멈춰서는 안된다.
restoreShowSelection()
})()
}
window.onkeydown = function(ev: KeyboardEvent) {
if (ev.key == "F5" || (ev.ctrlKey && ev.key == "r")) {
remote.getCurrentWindow().reload()
}
}
window.addEventListener("contextmenu", function(ev: MouseEvent) {
ev.preventDefault()
function parentById(ev: MouseEvent, id: string): HTMLElement {
for (let p of ev.composedPath()) {
let el = <HTMLElement>p
if (el.id == id) {
return el
}
}
return null
}
function parentByClassName(ev: MouseEvent, cls: string): HTMLElement {
for (let p of ev.composedPath()) {
let el = <HTMLElement>p
if (el.classList.contains(cls)) {
return el
}
}
return null
}
if (parentById(ev, "show-box")) {
let show = parentByClassName(ev, "item").id.split("-")[1]
let showMenu = new Menu()
let pinShowMenuItem = new MenuItem({
label: "상단에 고정",
click: uiEvent(function() {
let cur = currentShow()
pinShow(show)
reloadShows()
selectShow(cur)
restoreGroupSelection(cur)
}),
})
let unpinShowMenuItem = new MenuItem({
label: "상단에서 제거",
click: uiEvent(function() {
let cur = currentShow()
unpinShow(show)
reloadShows()
selectShow(cur)
restoreGroupSelection(cur)
}),
})
if (isPinnedShow(show)) {
showMenu.append(unpinShowMenuItem)
} else {
showMenu.append(pinShowMenuItem)
}
let openShowDir = new MenuItem({
label: "디렉토리 열기",
click: uiEvent(function() {
openDir(site.Show(show).Dir)
}),
})
showMenu.append(openShowDir)
showMenu.popup()
return
}
if (parentById(ev, "group-box")) {
let show = currentShow()
let ctg = currentCategory()
let grp = parentByClassName(ev, "item").id.split("-")[1]
let groupMenu = new Menu()
let pinGroupMenuItem = new MenuItem({
label: "상단에 고정",
click: uiEvent(function() {
pinGroup(show, ctg, grp)
reloadGroups()
restoreGroupSelection(show)
}),
})
let unpinGroupMenuItem = new MenuItem({
label: "상단에서 제거",
click: uiEvent(function() {
unpinGroup(show, ctg, grp)
reloadGroups()
restoreGroupSelection(show)
}),
})
if (isPinnedGroup(show, ctg, grp)) {
groupMenu.append(unpinGroupMenuItem)
} else {
groupMenu.append(pinGroupMenuItem)
}
let openGroupDir = new MenuItem({
label: "디렉토리 열기",
click: uiEvent(function() {
openDir(site.Show(show).Category(ctg).Group(grp).Dir)
}),
})
groupMenu.append(openGroupDir)
groupMenu.popup()
return
}
if (parentById(ev, "unit-box")) {
let show = currentShow()
let ctg = currentCategory()
let grp = currentGroup()
let unit = parentByClassName(ev, "item").id.split("-")[1]
let unitMenu = new Menu()
let pinUnitMenuItem = new MenuItem({
label: "상단에 고정",
click: uiEvent(function() {
pinUnit(show, ctg, grp, unit)
reloadUnits()
restoreUnitSelection(show, ctg, grp)
}),
})
let unpinUnitMenuItem = new MenuItem({
label: "상단에서 제거",
click: uiEvent(function() {
unpinUnit(show, ctg, grp, unit)
reloadUnits()
restoreUnitSelection(show, ctg, grp)
}),
})
if (isPinnedUnit(show, ctg, grp, unit)) {
unitMenu.append(unpinUnitMenuItem)
} else {
unitMenu.append(pinUnitMenuItem)
}
let openUnitDir = new MenuItem({
label: "디렉토리 열기",
click: uiEvent(function() {
openDir(site.Show(show).Category(ctg).Group(grp).Unit(unit).Dir)
}),
})
unitMenu.append(openUnitDir)
unitMenu.popup()
return
}
if (parentById(ev, "part-box")) {
let show = currentShow()
let ctg = currentCategory()
let grp = currentGroup()
let unit = currentUnit()
let part = parentByClassName(ev, "item").id.split("-")[1]
let partMenu = new Menu()
let openPartDir = new MenuItem({
label: "디렉토리 열기",
click: uiEvent(function() {
openDir(site.Show(show).Category(ctg).Group(grp).Unit(unit).Part(part).Dir)
}),
})
partMenu.append(openPartDir)
partMenu.popup()
return
}
if (parentById(ev, "task-box")) {
let show = currentShow()
let grp = currentGroup()
let unit = currentUnit()
let task = currentPart()
let div = parentByClassName(ev, "item")
let dir = div.dataset.dir
let taskMenu = new Menu()
let openTaskDir = new MenuItem({
label: "디렉토리 열기",
click: uiEvent(function() {
openDir(dir)
}),
})
taskMenu.append(openTaskDir)
taskMenu.popup()
return
}
})
// uiEvent 함수는 받아들인 함수를 이벤트 함수로 만들어 반환한다.
// 즉, 실행한 결과에 문제가 있었을 때 상태줄에 표시하고 로그로 기록하게 한다.
function uiEvent(f: () => void): () => void {
return function(): void {
try {
f()
} catch(err) {
console.log(err)
notify(err.message)
}
}
}
// ensureElementExist는 해당 HTML 엘리먼트가 존재하는지 검사한다.
// 존재하지 않는다면 에러를 낸다.
function ensureElementExist(id: string) {
let el = document.getElementById(id)
if (!el) {
throw Error(id + "가 존재하지 않습니다.")
}
}
export function openLogEv() {
uiEvent(function() {
openLog()
})()
}
function openLog() {
let m = document.getElementById("log")
m.style.display = "flex"
}
export function closeLogEv() {
uiEvent(function() {
closeLog()
})()
}
function closeLog() {
let m = document.getElementById("log")
m.style.display = "none"
}
// openModalEv는 사용자가 항목 추가 버튼을 눌렀을 때 그에 맞는 모달 창을 연다.
export function openModalEv(kind: string) {
if (kind == "group" && !currentShow()) {
notify("아직 쇼를 선택하지 않았습니다.")
return
}
if (kind == "unit" && !currentGroup()) {
notify("아직 그룹을 선택하지 않았습니다.")
return
}
if (kind == "part" && !currentUnit()) {
notify("아직 샷을 선택하지 않았습니다.")
return
}
if (kind == "task" && !currentPart()) {
notify("아직 태스크를 선택하지 않았습니다.")
return
}
uiEvent(function() {
openModal(kind)
})()
}
// openModal은 생성할 항목의 종류에 맞는 모달 창을 연다.
function openModal(kind: string) {
let m = document.getElementById("modal")
m.style.display = "block"
let input = <HTMLInputElement>document.getElementById("modal-input")
input.hidden = false
input.value = ""
let menuInput = <HTMLSelectElement>document.getElementById("modal-menu-input")
menuInput.hidden = true
menuInput.style.minWidth = "5rem"
let guide = <HTMLInputElement>document.getElementById("modal-guide")
guide.innerText = ""
let ctg = currentCategory()
if (kind == "part") {
input.hidden = true
menuInput.hidden = false
menuInput.innerText = ""
menuInput.style.minWidth = "10rem"
let parts = site.ValidParts(currentCategory())
for (let p of parts) {
let opt = document.createElement("option")
opt.text = p
menuInput.add(opt)
}
} else if (kind == "task") {
menuInput.hidden = false
menuInput.innerText = ""
let progs = Array()
try {
progs = site.ValidPrograms(currentCategory(), currentPart())
} catch(err) {
m.style.display = "none"
throw err
}
for (let p of progs) {
let opt = document.createElement("option")
opt.text = p
menuInput.add(opt)
}
guide.innerText = "기본 태스크 이름으로는 main을 사용해주세요."
}
let ctgLabel = site.CategoryLabel(ctg)
let kor = {
"show": "쇼",
"group": "그룹",
"unit": ctgLabel,
"part": ctgLabel + " 파트",
"task": ctgLabel + " 태스크",
}
input.placeholder = "생성 할 " + kor[kind] + " 이름"
function createItem() {
closeModal()
let input = <HTMLInputElement>document.getElementById("modal-input")
let name = input.value
if (!name) {
// 파트의 경우는 menuInput의 값을 사용하기 때문에 괜찮음
if (kind != "part") {
notify("생성할 항목의 이름을 설정하지 않았습니다.")
return
}
}
if (kind == "show") {
createShow(name)
} else if (kind == "group") {
createGroup(currentShow(), ctg, name)
} else if (kind == "unit") {
createUnit(currentShow(), ctg, currentGroup(), name)
} else if (kind == "part") {
let menuInput = <HTMLInputElement>document.getElementById("modal-menu-input")
name = menuInput.value
createPart(currentShow(), ctg, currentGroup(), currentUnit(), name)
} else if (kind == "task") {
let menuInput = <HTMLInputElement>document.getElementById("modal-menu-input")
let prog = menuInput.value
createTask(currentShow(), ctg, currentGroup(), currentUnit(), currentPart(), prog + "-" + name + "-v001")
}
}
let applyEv = uiEvent(function() {
createItem()
saveSelection()
})
input.onkeydown = function(ev: KeyboardEvent) { if (ev.key == "Enter") { applyEv() } }
menuInput.onkeydown = function(ev: KeyboardEvent) { if (ev.key == "Enter") { applyEv() } }
if (!menuInput.hidden) {
menuInput.focus()
} else {
input.focus()
}
let apply = document.getElementById("modal-apply")
apply.onclick = applyEv
}
// closeModalEv는 모달 사용중 사용자가 닫음 버튼을 눌렀을 때 모달을 닫는다.
export function closeModalEv() {
uiEvent(function() {
closeModal()
})()
}
// closeModal은 모달을 보이지 않도록 한다.
function closeModal() {
let m = document.getElementById("modal")
m.style.display = "none"
}
// log는 로그 창에 새로운 기록을 추가한다.
function log(text: string) {
let logContent = document.getElementById("log-content")
let pre = document.createElement("pre")
pre.style.color = "black"
pre.innerText = text
logContent.appendChild(pre)
}
// notify는 아래쪽 표시줄에 text를 표시한다. 표시된 내용은 로그 창에도 기록된다.
function notify(text: string): number {
log(text)
// notifier는 한줄의 메세지만 보일 수 있다.
// 마지막 줄을 보이기로 한다.
let lines = text.trim().split("\n")
let line = lines[lines.length - 1]
let notifier = document.getElementById("notifier")
notifier.innerText = line
// 현재 알림 메시지 아이디를 하나 증가시킨 후 반환한다.
currentNotificationID += 1
return currentNotificationID
}
// clearNotify는 아래쪽 표시줄에 기존에 표시된 내용을 지운다.
function clearNotify() {
notify("")
}
// loadCategory는 설정 디렉토리에 저장된 내 파트 값을 불러온다.
function loadCategory() {
let menu = <HTMLSelectElement>document.getElementById("category-menu")
let fname = configDir() + "/category.json"
if (!fs.existsSync(fname)) {
let ctg = site.ValidCategories()[0]
menu.value = ctg
document.getElementById("unit-label").innerText = site.CategoryLabel(ctg)
return
}
let data = fs.readFileSync(fname)
let ctg = data.toString("utf8")
menu.value = ctg
document.getElementById("unit-label").innerText = site.CategoryLabel(ctg)
}
// saveCategory는 내 파트로 설정된 값을 설정 디렉토리에 저장한다.
export function saveCategory() {
let menu = <HTMLSelectElement>document.getElementById("category-menu")
let ctg = menu.value
document.getElementById("unit-label").innerText = site.CategoryLabel(ctg)
let fname = configDir() + "/category.json"
fs.writeFileSync(fname, ctg)
selectShowEv(currentShow())
}
// loadSelection는 파일에서 마지막으로 선택했던 항목들을 다시 불러온다.
function loadSelection() {
let fname = configDir() + "/selection.json"
if (!fs.existsSync(fname)) {
return
}
let data = fs.readFileSync(fname)
selection = seltree.FromJSON(selection, JSON.parse(data.toString("utf8")))
}
// saveSelection는 현재 선택된 항목들을 파일로 저장한다.
function saveSelection() {
selectionChanged()
let data = JSON.stringify(selection, null, 1)
let fname = configDir() + "/selection.json"
fs.writeFileSync(fname, data)
}
function selectionChanged() {
let show = currentShow()
if (!show) {
return
}
let showSel = selection.Select(show)
let ctg = currentCategory()
if (!ctg) {
return
}
let ctgSel = showSel.Select(ctg)
let grp = currentGroup()
if (!grp) {
return
}
let grpSel = ctgSel.Select(grp)
let unit = currentUnit()
if (!unit) {
return
}
let unitSel = grpSel.Select(unit)
let part = currentPart()
if (!part) {
return
}
let partSel = unitSel.Select(part)
let vid = currentTask()
if (!vid) {
return
}
partSel.Select(vid)
}
// createShow는 하나의 쇼를 생성한다.
function createShow(show: string) {
site.CreateShow(show)
reloadShows()
selectShow(show)
}
// createGroup은 하나의 그룹을 생성한다.
function createGroup(show: string, ctg: string, grp: string) {
site.Show(show).Category(ctg).CreateGroup(grp)
reloadGroups()
selectGroup(grp)
}
// createUnit은 하나의 샷을 생성한다.
function createUnit(show: string, ctg: string, grp: string, unit: string) {
let units = parseUnitPattern(unit)
for (let u of units) {
site.Show(show).Category(ctg).Group(grp).CreateUnit(u)
}
reloadUnits()
if (units.length == 1) {
selectUnit(unit)
}
}
// parseUnitPattern 은 유닛 문자열이 여러 유닛을 의미하는 패턴인지 검사하고 만일 그렇다면 그 유닛 리스트를 반환한다.
// 그 패턴은 "min-max,inc" 이며, min과 max는 패딩을 가진 수, inc는 숫자여야한다. min과 max는 같은 길이를 가지고 있어야 한다.
// 예를 들어 unit이 "0010-0030,10" 이라는 문자열이라면 반환되는 값은 ["0010", "0020", "0030"] 이다.
// 만일 받아들인 값이 숫자가 아니거나 "0010" 같은 일반적인 패딩을 가진 수라면 ["0010"] 처럼 그 값만 리스트에 담겨 반환된다.
function parseUnitPattern(unit: string): string[] {
let m1 = unit.match(/,/g)
let m2 = unit.match(/-/g)
if (!m1 && !m2) {
// 패턴 문자열이 아닌 유닛 이름이다.
return [unit]
}
if (m1 && m1.length != 1) {
throw Error("invalid unit name pattern: two or more comma(,)")
}
if (m2 && m2.length != 1) {
throw Error("invalid unit name pattern: two or more dash(-)")
}
let [rng, inc] = unit.split(",")
let [min, max] = rng.split("-")
if (!isDigits(min)) {
throw Error("invalid unit name pattern: min value is not a number")
}
if (!isDigits(max)) {
throw Error("invalid unit name pattern: max value is not a number")
}
let start = parseInt(min)
let end = parseInt(max)
let n = parseInt(inc)
if (start > end) {
throw Error("invalid unit name pattern: min value is bigger than max value")
}
if (min.length != max.length) {
throw Error("invalid unit name pattern: min and max's paddings are different")
}
let pad = min.length
let units = []
for (let i = start; i <= end; i += n) {
units.push(String(i).padStart(pad, "0"))
}
return units
}
// isDigits는 받아들인 문자열이 숫자로만 이루어져 있는지 확인한다.
function isDigits(str: string): boolean {
let valids = "1234567890"
for (let i = 0; i < str.length; i++) {
let s = str[i]
if (!valids.includes(s)) {
return false
}
}
return true
}
// createPart는 하나의 샷 태스크를 생성한다.
function createPart(show: string, ctg: string, grp: string, unit: string, part: string) {
site.Show(show).Category(ctg).Group(grp).Unit(unit).CreatePart(part)
reloadParts()
selectPart(part)
}
// createTask는 하나의 샷 요소를 생성한다.
function createTask(show: string, ctg: string, grp: string, unit: string, part: string, vid: string) {
let [prog, task, ver] = vid.split("-")
site.Show(show).Category(ctg).Group(grp).Unit(unit).Part(part).CreateTask(prog, task, ver)
reloadTasks()
selectTask(vid)
}
// addCategoryMenuItems는 사용가능한 카테고리들을 내 태스크 메뉴에 추가한다.
function addCategoryMenuItems() {
let menu = <HTMLSelectElement>document.getElementById("category-menu")
if (!menu) {
throw Error("category-menu가 없습니다.")
}
for (let ctg of site.ValidCategories()) {
let opt = document.createElement("option")
opt.text = ctg
menu.add(opt)
}
}
// selectShowEv는 사용자가 쇼를 선택했을 때 그에 맞는 샷 리스트를 보인다.
function selectShowEv(show: string) {
uiEvent(function() {
selectShow(show)
reloadGroups()
restoreGroupSelection(show)
saveSelection()
})()
}
// selectShow는 사용자가 쇼를 선택했을 때 그에 맞는 샷 리스트를 보인다.
function selectShow(show: string) {
clearNotify()
clearGroups()
clearUnits()
clearParts()
clearTasks()
let box = document.getElementById("show-box")
let item = box.getElementsByClassName("selected")
if (item.length != 0) {
item[0].classList.remove("selected")
}
let selected = document.getElementById("show-" + show)
if (!selected) {
throw Error("쇼 선택 실패: " + show + " 가 존재하지 않습니다.")
}
selected.classList.add("selected")
reloadGroups()
}
// selectGroupEv는 사용자가 그룹을 선택했을 때 그에 맞는 유닛 리스트를 보인다.
function selectGroupEv(grp: string) {
uiEvent(function() {
selectGroup(grp)
reloadUnits()
restoreUnitSelection(currentShow(), currentCategory(), grp)
saveSelection()
})()
}
// selectGroup은 사용자가 그룹을 선택했을 때 그에 맞는 유닛 리스트를 보인다.
function selectGroup(grp: string) {
clearNotify()
clearUnits()
clearParts()
clearTasks()
let box = document.getElementById("group-box")
let item = box.getElementsByClassName("selected")
if (item.length != 0) {
item[0].classList.remove("selected")
}
let selected = document.getElementById("group-" + grp)
if (!selected) {
throw Error("그룹 선택 실패: " + grp + " 가 존재하지 않습니다.")
}
selected.classList.add("selected")
}
// selectUnitEv는 사용자가 샷을 선택했을 때 그에 맞는 태스크 리스트를 보인다.
// 추가로 내 태스크가 설정되어 있다면 그 태스크를 자동으로 선택해 준다.
function selectUnitEv(unit: string) {
uiEvent(function() {
selectUnit(unit)
reloadParts()
restorePartSelection(currentShow(), currentCategory(), currentGroup(), unit)
saveSelection()
})()
}
// selectUnit은 사용자가 샷을 선택했을 때 그에 맞는 태스크 리스트를 보인다.
// 추가로 내 태스크로 설정된 값이 있다면 그 태스크를 자동으로 선택해 준다.
function selectUnit(unit: string) {
clearNotify()
clearParts()
clearTasks()
let box = document.getElementById("unit-box")
let item = box.getElementsByClassName("selected")
if (item.length != 0) {
item[0].classList.remove("selected")
}
let selected = document.getElementById("unit-" + unit)
if (!selected) {
throw Error("유닛 선택 실패: " + unit + " 가 존재하지 않습니다.")
}
selected.classList.add("selected")
}
// selectPartEv는 태스크를 선택했을 때 그 안의 요소 리스트를 보인다.
function selectPartEv(part: string) {
uiEvent(function() {
selectPart(part)
reloadTasks()
restoreTaskSelection(currentShow(), currentCategory(), currentGroup(), currentUnit(), part)
saveSelection()
})()
}
// selectPart는 태스크를 선택했을 때 그 안의 요소 리스트를 보인다.
function selectPart(part: string) {
clearNotify()
clearTasks()
let box = document.getElementById("part-box")
let item = box.getElementsByClassName("selected")
if (item.length != 0) {
item[0].classList.remove("selected")
}
let selected = document.getElementById("part-" + part)
if (!selected) {
throw Error("파트 선택 실패: " + part + " 가 존재하지 않습니다.")
}
selected.classList.add("selected")
}
// selectTaskEv는 요소를 선택했을 때 그 선택을 표시한다.
function selectTaskEv(vid: string) {
uiEvent(function() {
selectTask(vid)
saveSelection()
})()
}
// selectTask는 요소를 선택했을 때 그 선택을 표시한다.
function selectTask(vid: string) {
clearNotify()
let box = document.getElementById("task-box")
let item = box.getElementsByClassName("selected")
if (item.length != 0) {
item[0].classList.remove("selected")
}
let selected = document.getElementById("task-" + vid)
if (!selected) {
throw Error("태스크 선택 실패: " + vid + " 가 존재하지 않습니다.")
}
selected.classList.add("selected")
}
// restoreShowSelection은 마지막으로 선택되었던 쇼로 선택을 되돌린다.
// 기억된 하위 요소들도 함께 되돌린다.
// 마지막 선택으로 돌리지 못하더라도 큰 문제는 아니기 때문에 실패에 대해 알리지 않는다.
function restoreShowSelection() {
let show = selection.Selected()
if (!show) {
return
}
try {
selectShow(show)
} catch(err) {
log(err.message)
return
}
reloadGroups()
restoreGroupSelection(show)
}
// restoreGroupSelection은 해당 쇼에서 마지막으로 선택되었던 그룹으로 선택을 되돌린다.
// 기억된 하위 요소들도 함께 되돌린다.
// 마지막 선택으로 돌리지 못하더라도 큰 문제는 아니기 때문에 실패에 대해 알리지 않는다.
function restoreGroupSelection(show: string) {
let ctg = currentCategory()
let ctgSel = selection.Select(show).Select(ctg)
let grp = ctgSel.Selected()
if (!grp) {
return
}
try {
selectGroup(grp)
} catch(err) {
log(err.message)
return
}
reloadUnits()
restoreUnitSelection(show, ctg, grp)
}
// restoreUnitSelection은 해당 그룹에서 마지막으로 선택되었던 유닛으로 선택을 되돌린다.
// 기억된 하위 요소들도 함께 되돌린다.
// 마지막 선택으로 돌리지 못하더라도 큰 문제는 아니기 때문에 실패에 대해 알리지 않는다.
function restoreUnitSelection(show: string, ctg: string, grp: string) {
let grpSel = selection.Select(show).Select(ctg).Select(grp)
let unit = grpSel.Selected()
if (!unit) {
return
}
try {
selectUnit(unit)
} catch(err) {
log(err.message)
return
}
reloadParts()
restorePartSelection(show, ctg, grp, unit)
}
// restorePartSelection은 해당 유닛에서 마지막으로 선택되었던 파트로 선택을 되돌린다.
// 기억된 하위 요소들도 함께 되돌린다.
// 마지막 선택으로 돌리지 못하더라도 큰 문제는 아니기 때문에 실패에 대해 알리지 않는다.
function restorePartSelection(show: string, ctg: string, grp: string, unit: string) {
let unitSel = selection.Select(show).Select(ctg).Select(grp).Select(unit)
let part = unitSel.Selected()
if (!part) {
return
}
try {
selectPart(part)
} catch(err) {
log(err.message)
return
}
reloadTasks()
restoreTaskSelection(show, ctg, grp, unit, part)
}
// restoreTaskSelection은 해당 파트에서 마지막으로 선택되었던 (버전 포함) 태스크로 선택을 되돌린다.
// 마지막 선택으로 돌리지 못하더라도 큰 문제는 아니기 때문에 실패에 대해 알리지 않는다.
function restoreTaskSelection(show: string, ctg: string, grp: string, unit: string, part: string) {
let partSel = selection.Select(show).Select(ctg).Select(grp).Select(unit).Select(part)
let vid = partSel.Selected()
if (!vid) {
return
}
let [prog, task, ver] = vid.split("-")
if (ver) {
let tid = prog + "-" + task
toggleVersionVisibility(tid)
}
try {
selectTask(vid)
} catch(err) {
log(err.message)
return
}
}
// currentShow는 현재 선택된 쇼 이름을 반환한다.
function currentShow(): string {
return selectedItemValue("show-box")
}
// currentCategory는 현재 선택된 카테고리 이름을 반환한다.
function currentCategory(): string {
let menu = <HTMLSelectElement>document.getElementById("category-menu")
return menu.value
}
// currentGroup은 현재 선택된 그룹 이름을 반환한다.
function currentGroup(): string {
return selectedItemValue("group-box")
}
// currentUnit은 현재 선택된 샷 이름을 반환한다.
function currentUnit(): string {
return selectedItemValue("unit-box")
}
// currentPart는 현재 선택된 샷 파트 이름을 반환한다.
function currentPart(): string {
return selectedItemValue("part-box")
}
// currentTask는 현재 선택된 샷 태스크의 버전 아이디를 반환한다.
function currentTask(): string {
let vid = selectedItemValue("task-box")
if (!vid) {
return null
}
// vid는 "{prog}-{task}-{version}" 형식이다.
return vid
}
// selectedItemValue는 특정 'item-box' HTML 요소에서 선틱된 값을 반환한다.
function selectedItemValue(boxId): string {
let box = document.getElementById(boxId)
if (!box) {
throw Error(boxId + "가 없습니다.")
}
let items = Array.from(box.getElementsByClassName("item"))
if (!items) {
return null
}
for (let i in items) {
let item = <HTMLElement>items[i]
if (item.classList.contains("selected")) {
return item.dataset.val
}
}
return null
}
// newBoxItem은 box 클래스 안에서 사용될 item HTML 요소를 생성한다.
// 받아들이는 두 인수는 각각 왼쪽(메인)과 오른쪽(서브)에 적힐 내용이다.
function newBoxItem(val, mark): HTMLElement {
let tmpl = <HTMLTemplateElement>document.getElementById("item-tmpl")
let frag = document.importNode(tmpl.content, true)
let item = frag.querySelector("div")
item.getElementsByClassName("item-val")[0].textContent = val
item.getElementsByClassName("item-mark")[0].textContent = mark
return item
}
// reloadShows는 쇼를 다시 부른다.
function reloadShows() {
let box = document.getElementById("show-box")
box.innerText = ""
let shows = names(site.Shows())
let byPinAndName = function(a, b) {
let ap = isPinnedShow(a)
let bp = isPinnedShow(b)
if ((ap && bp) || (!ap && !bp)) {
return byName(a, b)
}
if (ap) {
return -1
} else {
return 1
}
}
shows.sort(byPinAndName)
for (let show of shows) {
let mark = ""
if (isPinnedShow(show)) {
mark = "-"
}
let div = newBoxItem(show, mark)
div.id = "show-" + show
div.dataset.val = show
div.onclick = function() { selectShowEv(show) }
box.append(div)
}
}
// reloadGroups는 해당 쇼의 그룹을 다시 부른다.
function reloadGroups() {
let show = currentShow()
if (!show) {
throw Error("선택된 쇼가 없습니다.")
}
let ctg = currentCategory()
let box = document.getElementById("group-box")
box.innerText = ""
let groups = names(site.Show(show).Category(ctg).Groups())
let byPinAndName = function(a, b) {
let ap = isPinnedGroup(show, ctg, a)
let bp = isPinnedGroup(show, ctg, b)
if ((ap && bp) || (!ap && !bp)) {
return byName(a, b)
}
if (ap) {
return -1
} else {
return 1
}
}
groups.sort(byPinAndName)
for (let grp of groups) {
let mark = ""
if (isPinnedGroup(show, ctg, grp)) {
mark = "-"
}
let div = newBoxItem(grp, mark)
div.id = "group-" + grp
div.dataset.val = grp
div.onclick = function() { selectGroupEv(grp) }
box.append(div)
}
}
// reloadUnits는 해당 쇼의 샷을 다시 부른다.
function reloadUnits() {
let show = currentShow()
if (!show) {
throw Error("선택된 쇼가 없습니다.")
}
let ctg = currentCategory()
let grp = currentGroup()
if (!grp) {
throw Error("선택된 그룹이 없습니다.")
}
let box = document.getElementById("unit-box")
box.innerText = ""
let units = names(site.Show(show).Category(ctg).Group(grp).Units())
let byPinAndName = function(a, b) {
let ap = isPinnedUnit(show, ctg, grp, a)
let bp = isPinnedUnit(show, ctg, grp, b)
if ((ap && bp) || (!ap && !bp)) {
return byName(a, b)
}
if (ap) {
return -1
} else {
return 1
}