index.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import * as audio from '../audio';
  2. import * as level from '../level';
  3. import * as util from '../util';
  4. import * as youtube from '../youtube';
  5. import { LevelEditor } from './level';
  6. import { LevelSetEditor } from './levelSet';
  7. class ConfigEditor {
  8. containerElement: HTMLDivElement;
  9. openScreen: HTMLDivElement;
  10. configScreen: HTMLDivElement;
  11. openFileElement: HTMLInputElement;
  12. navigationElement: HTMLElement;
  13. levelsElement: HTMLDivElement;
  14. levelEditor: LevelEditor;
  15. audioManager: audio.AudioManager;
  16. config: level.Config | null;
  17. currentLevel: level.Level | null;
  18. constructor() {
  19. this.containerElement = util.getElement(document, '#container');
  20. this.openScreen = util.getElement(document, '#open-screen');
  21. this.openFileElement = util.getElement(this.openScreen, '#open-file');
  22. this.openFileElement.addEventListener('change', () => {
  23. this.load();
  24. });
  25. util.getElement(document, '#open-new').addEventListener('click', () => {
  26. this.create();
  27. });
  28. this.configScreen = util.getElement(document, '#config-screen');
  29. this.navigationElement = util.getElement(document, '#config-navigation');
  30. this.levelsElement = util.getElement(document, '#config-levels');
  31. util.getElement(document, '#config-add').addEventListener('click', () => {
  32. this.addLevelSet();
  33. });
  34. util
  35. .getElement(document, '#config-download')
  36. .addEventListener('click', () => {
  37. this.download();
  38. });
  39. util.getElement(document, '#config-close').addEventListener('click', () => {
  40. this.close();
  41. });
  42. this.levelEditor = new LevelEditor(
  43. () => {
  44. this.containerElement.classList.remove('editing');
  45. this.currentLevel = null;
  46. },
  47. (lines) => {
  48. this.currentLevel!.lines = lines;
  49. this.persistConfig();
  50. }
  51. );
  52. this.audioManager = new audio.AudioManager();
  53. this.config = null;
  54. this.currentLevel = null;
  55. const storedConfig = localStorage.getItem('LEVELS_JSON');
  56. this.updateConfig(storedConfig === null ? null : JSON.parse(storedConfig));
  57. }
  58. updateConfig(config: level.Config | null) {
  59. this.config = config;
  60. this.containerElement.classList.toggle('loaded', config !== null);
  61. this.persistConfig();
  62. this.render();
  63. }
  64. persistConfig() {
  65. if (this.config !== null) {
  66. localStorage.setItem('LEVELS_JSON', JSON.stringify(this.config));
  67. } else {
  68. localStorage.removeItem('LEVELS_JSON');
  69. }
  70. }
  71. create() {
  72. this.updateConfig({
  73. background: 'royalblue',
  74. selectMusic: null,
  75. baseColor: 'white',
  76. highlightColor: 'cyan',
  77. contrastColor: 'black',
  78. selectSound: 'select.wav',
  79. decideSound: 'decide.wav',
  80. levelSets: [],
  81. });
  82. }
  83. load() {
  84. const file = this.openFileElement.files?.[0];
  85. if (file !== undefined) {
  86. file.arrayBuffer().then((buffer) => {
  87. const decoder = new TextDecoder();
  88. this.updateConfig(JSON.parse(decoder.decode(buffer)));
  89. });
  90. }
  91. }
  92. download() {
  93. const a = document.createElement('a');
  94. const url = URL.createObjectURL(new Blob([JSON.stringify(this.config)]));
  95. a.href = url;
  96. a.download = 'levels.json';
  97. a.click();
  98. URL.revokeObjectURL(url);
  99. }
  100. close() {
  101. if (confirm('Are you sure you want to close?')) {
  102. this.updateConfig(null);
  103. }
  104. }
  105. render() {
  106. if (this.config === null) {
  107. return;
  108. }
  109. this.levelsElement.textContent = '';
  110. this.navigationElement.textContent = '';
  111. this.config.levelSets.forEach((levelSet, index) => {
  112. const a = document.createElement('a');
  113. a.href = `#level-set-${index}`;
  114. a.textContent = levelSet.name;
  115. this.navigationElement.appendChild(a);
  116. new LevelSetEditor(
  117. this.levelsElement,
  118. index,
  119. levelSet,
  120. () => {
  121. this.persistConfig();
  122. this.render();
  123. },
  124. (level) => this.edit(level),
  125. (index, direction) => this.moveLevelSet(index, direction),
  126. (index) => this.removeLevelSet(index)
  127. );
  128. });
  129. }
  130. async edit(level: level.Level) {
  131. const track = level.audio ? await this.loadAudio(level.audio) : null;
  132. this.currentLevel = level;
  133. this.containerElement.classList.add('editing');
  134. this.levelEditor.load(level.name, level.lines, track);
  135. }
  136. async loadAudio(url: string): Promise<audio.Track> {
  137. const youtubeContainer = util.getElement(document, '#youtube');
  138. youtubeContainer.textContent = '';
  139. const videoId = youtube.getVideoId(url);
  140. if (videoId !== null) {
  141. const element = document.createElement('div');
  142. youtubeContainer.appendChild(element);
  143. return await this.audioManager.loadTrackFromYoutube(
  144. videoId,
  145. element,
  146. () => {}
  147. );
  148. }
  149. const fileLoader = document.createElement('input');
  150. fileLoader.type = 'file';
  151. fileLoader.accept = 'audio/*';
  152. return await new Promise((resolve, reject) => {
  153. fileLoader.addEventListener('change', () => {
  154. const file = fileLoader.files![0];
  155. if (file !== null) {
  156. resolve(this.audioManager.loadTrackFromFile(file));
  157. } else {
  158. reject('Cancelled');
  159. }
  160. });
  161. fileLoader.click();
  162. });
  163. }
  164. addLevelSet() {
  165. if (this.config !== null) {
  166. this.config.levelSets.push({
  167. name: 'New Level Set',
  168. levels: [],
  169. });
  170. this.persistConfig();
  171. this.render();
  172. }
  173. }
  174. moveLevelSet(index: number, direction: number): void {
  175. if (this.config === null) {
  176. return;
  177. }
  178. const target = index + direction;
  179. if (target < 0) {
  180. return;
  181. }
  182. if (target >= this.config.levelSets.length) {
  183. return;
  184. }
  185. const level = this.config.levelSets[index];
  186. this.config.levelSets.splice(index, 1);
  187. this.config.levelSets.splice(target, 0, level);
  188. this.persistConfig();
  189. this.render();
  190. }
  191. removeLevelSet(index: number): void {
  192. if (this.config === null) {
  193. return;
  194. }
  195. const level = this.config.levelSets[index];
  196. if (confirm(`Are you sure you want to remove ${level.name}?`)) {
  197. this.config.levelSets.splice(index, 1);
  198. this.persistConfig();
  199. this.render();
  200. }
  201. }
  202. }
  203. const editor = new ConfigEditor();
  204. // @ts-ignore
  205. window.editor = editor;