Reimu / AAC-Utils

// ==UserScript==
// @name         AAC-Utils
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @copyright    2022, Reimu(https://openuserjs.org/users/Reimu)
// @license      MIT
// @description  (Perhaps not so) Small fixes and utility functions for Anime Academy
// @author       Nick S. aka. Slash
// @include      /^https?:\/\/(www\.)?anime\.academy\/chat/
// @icon         https://www.google.com/s2/favicons?domain=anime.academy
// @updateURL    https://openuserjs.org/meta/Reimu/AAC-Utils.meta.js
// @downloadURL  https://openuserjs.org/install/Reimu/AAC-Utils.user.js
// @supportURL   https://openuserjs.org/scripts/Reimu/AAC-Utils/issues
// @setupURL     https://openuserjs.org/install/Reimu/AAC-Utils.user.js
// @grant none
// ==/UserScript==

(async function () {
  'use strict';

  /************************************
   *
   *
   * CSS
   *
   *
   ************************************/

  function addGlobalStyle(css) {
    let head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) {
      return;
    }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
  }

  addGlobalStyle(
    '.autocomplete-flex {' +
      '  background-color: #484b52;' +
      '  font-weight: 600;' +
      '  display: flex;' +
      '  align-items: center;' +
      '  gap: 10px;' +
      '  padding: 5px;' +
      '}' +
      '.autocomplete-flex:hover {' +
      '  background-color: #5f646e;' +
      '}' +
      '' +
      '.autocomplete-flex:focus {' +
      '  background-color: #5f646e;' +
      '}' +
      '' +
      '.name-link:hover {' +
      '  text-decoration: none;' +
      '}' +
      '.autocomplete-icon {' +
      '  width: 45px !important;' +
      '  height: 45px !important;' +
      '  border-radius: 50%;' +
      '}' +
      '.chatMessage {' +
      '  transition: padding 0.4s ease 0s;' +
      '}' +
      '.chatMessage:hover {' +
      '  padding-top: 5px;' +
      '  padding-bottom: 5px;' +
      '  position: relative;' +
      '  background-color: #5f646e !important;' +
      '  border-radius: 3px;' +
      '  transition: padding 0.4s ease 0s;' +
      '}' +
      '.messageIcons {' +
      '  display: flex;' +
      '  justify-content: center;' +
      '  align-items: center;' +
      '  height: 32px;' +
      '  width: 32px;' +
      '  background-color: #484b52;' +
      '  position: absolute;' +
      '  right: 0;' +
      '  top: -20px;' +
      '  display: none;' +
      '  border-radius: 3px;' +
      '}' +
      '.messageIcons:hover {' +
      '  background-color: #5f646e;' +
      '  filter: drop-shadow(5px 7px 14px black);' +
      '}' +
      '' +
      '.collapsible-wrap {' +
      '  margin: 10px 0 10px 0;' +
      '}' +
      '' +
      '.collapsible-button {' +
      '  cursor: pointer;' +
      '  padding: 18px;' +
      '  width: 100%;' +
      '  text-align: left;' +
      '  outline: none;' +
      '  font-size: 15px;' +
      '}' +
      '' +
      '.collapsible-button:focus {' +
      '  transition: background-color 0.5s ease;' +
      '  background-color: #3e1e80 !important;' +
      '}' +
      '' +
      '.collapsible-button:hover {' +
      '  transition: background-color 0.5s ease;' +
      '}' +
      '' +
      '.active {' +
      '  border-bottom-right-radius: 0 !important;' +
      '  border-bottom-left-radius: 0 !important;' +
      '  background-color: #3e1e80 !important;' +
      '}' +
      '' +
      '.collapsible-button:after {' +
      "  content: '\\002B';" +
      '  font-weight: bold;' +
      '  float: right;' +
      '  margin-left: 5px;' +
      '}' +
      '' +
      '.active:after {' +
      "  content: '\\2212';" +
      '}' +
      '' +
      '.collapsible-content {' +
      '  padding: 0 10px;' +
      '  max-height: 0;' +
      '  overflow: hidden;' +
      '  transition: max-height 0.2s ease-out;' +
      '  background-color: #2c2f33;' +
      '  border-bottom-left-radius: 3px;' +
      '  border-bottom-right-radius: 3px;' +
      '}' +
      '' +
      '.avatar-grid {' +
      '  width: 100%;' +
      '  display: grid;' +
      '  grid-template-columns: repeat(auto-fill, 8em);' +
      '  grid-gap: 10px 15px;' +
      '}' +
      '' +
      '.avatar-div {' +
      '  height: 80px;' +
      '  width: 80px;' +
      '  display: flex;' +
      '  align-items: center;' +
      '  justify-content: center;' +
      '  gap: 5px;' +
      '}' +
      '' +
      '.avatar-div:hover {' +
      '  background-color: #484b52;' +
      '  transition: background-color 0.5s ease;' +
      '  border-radius: 3px;' +
      '}' +
      '' +
      '.avatarIcon {' +
      '  height: 20px;' +
      '  width: 20px;' +
      '  background-color: #3e1e80;' +
      '  border-radius: 50%;' +
      '  display: none;' +
      '  align-items: center;' +
      '  justify-content: center;' +
      '}' +
      '' +
      '.avatarIcon:hover {' +
      '  background-color: #6b36d9;' +
      '  filter: drop-shadow(5px 7px 14px #2c2f33);' +
      '  transition: background-color 0.5s ease;' +
      '}' +
      '' +
      '.ionicon {' +
      '  width: 13px;' +
      '  height: 13px;' +
      '  fill: #ddd;' +
      '}' +
      '' +
      '.collection-selection {' +
      '  position: absolute;' +
      '  z-index: 10000;' +
      '  margin-left: auto;' +
      '  margin-right: auto;' +
      '  right: 0;' +
      '  left: 0;' +
      '  top: 80px;' +
      '  width: 300px;' +
      '  background-color: #484b52;' +
      '  padding: 10px;' +
      '  text-align: center;' +
      '  overflow: hidden scroll;' +
      '  border-radius: 3px;' +
      '  max-height: 200px;' +
      '}' +
      '' +
      '.collection-selectable {' +
      '  font-weight: 700;' +
      '  padding: 10px 0;' +
      '  margin-bottom: 5px;' +
      '}' +
      '' +
      '.collection-selectable:hover {' +
      '  background-color: #3e1e80;' +
      '  border-radius: 3px;' +
      '  transition: background-color 0.5s ease;' +
      '  filter: drop-shadow(5px 7px 14px #2c2f33);' +
      '}' +
      '' +
      '#createCollectionBtn {' +
      '  font-weight: 700;' +
      '}' +
      '' +
      '.removeCategory-btn {' +
      '  background-color: #fd2f2f;' +
      '}' +
      '' +
      '.request-delete-collection {' +
      '  position: absolute;' +
      '  z-index: 10000;' +
      '  margin-left: auto;' +
      '  margin-right: auto;' +
      '  right: 0;' +
      '  left: 0;' +
      '  top: 80px;' +
      '  width: 300px;' +
      '  background-color: #484b52;' +
      '  padding: 10px;' +
      '  text-align: center;' +
      '  overflow: hidden scroll;' +
      '  border-radius: 3px;' +
      '  max-height: 200px;' +
      '}' +
      '' +
      '.message-image {' +
      '  width: 100%;' +
      '  height: auto;' +
      '  border-radius: 3px;' +
      '}' +
      '' +
      '.message-image-container {' +
      '  margin-top: 5px;' +
      '}' +
      '' +
      '#username-results {' +
      '  border: none;' +
      '  max-height: 200px;' +
      '  overflow: hidden scroll;' +
      '  max-width: 30%;' +
      '  position: absolute;' +
      '  left: 0;' +
      '  right: 0;' +
      '  bottom: calc(100% + 8px);' +
      '  margin-left: 8px;' +
      '  border-radius: 5px;' +
      '}' +
      ''
  );

  // Necessary for autoreconnect
  window.onbeforeunload = null;

  const scope = angular.element(document.getElementById('topbar')).scope();

  /************************************
   *
   *
   * Global Socket Hook
   *
   *
   ************************************/

  const globalSocketReady = new Event('globalSocketReady');

  io.Socket.prototype.o_emit = io.Socket.prototype.o_emit || io.Socket.prototype.emit;
  io.Socket.prototype.emit = function (eventName, ...args) {
    if (!window.socket) {
      window.socket = this;
      window.dispatchEvent(globalSocketReady);
    }

    window.dispatchEvent(new CustomEvent('socketEmit', { detail: { eventName: eventName, args: [...args] } }));

    return this.o_emit(eventName, ...args);
  };

  /************************************
   *
   *
   * Autoreconnnect
   *
   *
   ************************************/

  const disconnectReasons = [
    'transport error',
    'transport close',
    'io client disconnect',
    'io server disconnect',
    'ping timeout',
  ];

  const disconnected = JSON.parse(localStorage.getItem('disconnected'));

  if (disconnected) {
    localStorage.setItem('disconnected', JSON.stringify(false));
    window.addEventListener('globalSocketReady', () => {
      setTimeout(() => {
        window.socket.emit('moveAvatar', JSON.parse(localStorage.getItem('avatarPosition')));
      }, 1500);
    });
  }

  window.addEventListener('socketEmit', (event) => {
    if (event.detail.eventName === 'moveAvatar') {
      localStorage.setItem('avatarPosition', JSON.stringify(event.detail.args[0]));
    }
    if (event.detail.eventName === 'disconnect' && disconnectReasons.includes(event.detail.args[0])) {
      localStorage.setItem('disconnected', JSON.stringify(true));
      location.reload();
    }
  });

  /************************************
   *
   *
   * Reload Chat History
   *
   *
   ************************************/

  const maxStoredMessagesPerRoom = 50;
  const maxMessageAge = 1000 * 60 * 30; // 30 Minutes

  window.addEventListener('globalSocketReady', () => {
    window.socket.on('updateChatLines', (data) => {
      const currentRoom = window.location.href.split('=')[1];

      let roomData;

      if (!localStorage.hasOwnProperty('rooms')) {
        localStorage.setItem('rooms', JSON.stringify({}));
      }
      if (!JSON.parse(localStorage.getItem('rooms')).hasOwnProperty(currentRoom)) {
        roomData = JSON.parse(localStorage.getItem('rooms'));
        roomData[currentRoom] = {
          messages: [],
        };
        roomData = JSON.stringify(roomData);
        localStorage.setItem('rooms', roomData);
      }

      roomData = JSON.parse(localStorage.getItem('rooms'));
      if (data.user !== 'System') roomData[currentRoom].messages.push(data);
      if (roomData[currentRoom].messages.length > maxStoredMessagesPerRoom) roomData[currentRoom].messages.splice(0, 1);

      roomData = JSON.stringify(roomData);
      localStorage.setItem('rooms', roomData);
    });

    window.addEventListener('socketEmit', (event) => {
      if (event.detail.eventName === 'changeRoom') {
        setTimeout(() => {
          restoreChatlogs();
        }, 100);
      }
    });

    restoreChatlogs();
  });

  function restoreChatlogs() {
    let currentRoomChatlogs = undefined;

    if (JSON.parse(localStorage.getItem('rooms'))) {
      if (JSON.parse(localStorage.getItem('rooms'))[window.location.href.split('=')[1]]) {
        currentRoomChatlogs = JSON.parse(localStorage.getItem('rooms'))[window.location.href.split('=')[1]].messages;
      }
    } else {
      return;
    }

    if (currentRoomChatlogs) {
      setTimeout(() => {
        for (const messageData of currentRoomChatlogs) {
          if (messageData.user !== 'System' && Date.now() - messageData.timestamp < maxMessageAge) {
            const message = {
              hasPremium: messageData.hasPremium,
              msg: messageData.chatLine,
              festername: messageData.festername,
              house: messageData.house ? messageData.house : null,
              color: undefined,
              user: messageData.user + ': ',
              timestamp: `${new Date(messageData.timestamp).toLocaleDateString('de-DE')} - ${new Date(
                messageData.timestamp
              )
                .toLocaleTimeString('de-DE')
                .slice(0, -3)} Uhr`,
            };

            scope.chatmsgs.push(message);
            document.getElementById('topbar').click(); // Yeah I have absolutely no fucking clue.
          }
        }
      }, 1500);
    }
  }

  /************************************
   *
   *
   * Chat History Garbage Collector
   *
   *
   ************************************/

  if (localStorage.hasOwnProperty('rooms')) {
    const chatLogs = JSON.parse(localStorage.getItem('rooms'));

    if (chatLogs) {
      for (const room in chatLogs) {
        const tooOldMessages = [];
        for (const message in chatLogs[room].messages) {
          if (Date.now() - chatLogs[room].messages[message].timestamp > maxMessageAge) {
            tooOldMessages.push(message);
          }
        }

        for (const index of tooOldMessages) {
          chatLogs[room].messages.splice(index, 1);
        }

        if (chatLogs[room].messages.length === 0) {
          delete chatLogs[room];
        }
      }
    }

    localStorage.setItem('rooms', JSON.stringify(chatLogs));
  }

  /************************************
   *
   *
   * Autocomplete Usernames
   *
   *
   ************************************/

  const chatArea = document.getElementById('graphicChatArea');
  chatArea.style.border = 'none';

  const messageForm = document.getElementsByName('chatMsgForm')[0];
  const messageInput = document.getElementById('chatline');

  messageInput.setAttribute('onKeyUp', 'showResults(this.value)');

  const resultDiv = document.createElement('div');
  resultDiv.setAttribute('id', 'username-results');

  messageForm.appendChild(resultDiv);

  function showResults(value) {
    const result = document.getElementById('username-results');

    result.style.display = 'block';

    if (!value.includes('@')) {
      result.style.display = 'none';
    }

    result.innerHTML = '';

    let list = '';

    const users = window.autocompleteMatch(value);

    for (const user in users) {
      list += `<a class="name-link"><div class="autocomplete-flex autocomplete-list" onclick="replaceName(this.children[1].innerHTML)" tabindex="0"><img class="autocomplete-icon" src="/img/publicimg/skinthumbnails/${
        users[user].imgthumb
      }"><div class="name-wrap">${users[user].username.split('@')[1]}</div></div></a>`;
    }

    result.innerHTML = `<ul style="padding: 0px; margin: 0px;">${list}</ul>`;

    const autocompleteListElements = document.getElementsByClassName('autocomplete-list');

    for (const element of autocompleteListElements) {
      element.addEventListener('keyup', function (event) {
        if (event.keyCode === 13) {
          window.replaceName(this.children[1].innerHTML);
        }
      });
    }
  }

  function autocompleteMatch(input) {
    if (input === '') {
      return [];
    }

    const scope = angular.element(document.getElementById('topbar')).scope();
    const users = scope.chatterlist.filter((user) => user.room === scope.roomData.name);
    const userObjects = users.map((user) => {
      return { ...user, username: `@${user.username}` };
    });

    const reg = new RegExp(`\\B@(${input.split('@')[1]}.*)$`, 'i');
    return userObjects.filter((user) => {
      if (user.username.match(reg)) {
        return user;
      }
    });
  }

  function replaceName(username) {
    const scope = angular.element(document.getElementById('topbar')).scope();
    const messageInput = document.getElementById('chatline');

    scope.chatline = scope.chatline.replace(/\B@(\w*)$/, `${username} `);

    const resultDiv = document.getElementById('username-results');
    resultDiv.style.display = 'none';

    messageInput.focus();
  }

  window.replaceName = replaceName;
  window.showResults = showResults;
  window.autocompleteMatch = autocompleteMatch;

  /************************************
   *
   *
   * Save Enable-Video Preferences
   *
   *
   ************************************/

  const enableVideosInput = document.getElementById('checkAllowVideo');
  const enableVideos =
    JSON.parse(localStorage.getItem('enableVideos')) === undefined
      ? true
      : JSON.parse(localStorage.getItem('enableVideos'));

  scope.allowVideos = enableVideos;
  enableVideosInput.checked = enableVideos;

  enableVideosInput.addEventListener('change', function () {
    localStorage.setItem('enableVideos', JSON.stringify(this.checked));
  });

  /************************************
   *
   *
   * Save Microphone color
   *
   *
   ************************************/

  const microphoneBackgroundColor = JSON.parse(localStorage.getItem('microphoneBackgroundColor'));
  const microphoneTextColor = JSON.parse(localStorage.getItem('microphoneTextColor'));

  if (microphoneBackgroundColor && microphoneTextColor) {
    window.addEventListener('globalSocketReady', () => {
      setTimeout(() => {
        window.socket.emit('applyMicrophoneColor', {
          microphoneBackgroundColor: microphoneBackgroundColor,
          microphoneTextColor: microphoneTextColor,
        });
      }, 1500);
    });
  }

  window.addEventListener('socketEmit', (event) => {
    if (event.detail.eventName === 'applyMicrophoneColor') {
      localStorage.setItem('microphoneBackgroundColor', JSON.stringify(event.detail.args[0].microphoneBackgroundColor));
      localStorage.setItem('microphoneTextColor', JSON.stringify(event.detail.args[0].microphoneTextColor));
    }
  });

  /************************************
   *
   *
   * Fix Loading-DM Issue
   *
   *
   ************************************/

  window.addEventListener('globalSocketReady', () => {
    window.socket.on('getPNList', () => {
      const scope = angular.element(document.getElementById('topbar')).scope();
      scope.loading = false;
    });
  });

  /************************************
   *
   *
   * Reply-Message / View Images in chat
   *
   *
   ************************************/

  const messageDiv = document.getElementsByClassName('chatverlauf')[0];

  const messageObserverConfig = { childList: true };

  const messagesMutationCallback = function (mutationsList) {
    for (const mutation of mutationsList) {
      // Reply-Message

      if (mutation.addedNodes.length) {
        const message = mutation.addedNodes[0];
        const chatMessageDiv = message.querySelectorAll(`[ng-bind-html^='chatmsg.msg']`)[0];
        const messageContent = chatMessageDiv.textContent;
        const isSystemMessage = message.classList.contains('systemMsg');
        const isEmojiMessage = message.querySelectorAll(`[ng-if='chatmsg.emojiImage']`).length > 0;
        const isOwnMessage = messageContent.startsWith(': ');

        message.style.paddingLeft = '1px';

        if (isEmojiMessage) {
          continue;
        }

        if (isSystemMessage) {
          continue;
        }

        message.classList.add('chatMessage');

        const messageIconsDiv = document.createElement('div');

        messageIconsDiv.classList.add('messageIcons');
        messageIconsDiv.innerHTML =
          '<svg aria-hidden="true" class="svg-inline--fa fa-quote-right fa-w-16" focusable="false" data-prefix="fa" data-icon="quote-right" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="#d2d2d2" d="M464 32H336c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48zm-288 0H48C21.5 32 0 53.5 0 80v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48z"></path></svg>';

        message.appendChild(messageIconsDiv);

        message.addEventListener('mouseover', function () {
          const iconDiv = this.children[this.children.length - 1];
          iconDiv.style.display = 'flex';
        });

        message.addEventListener('mouseout', function () {
          const iconDiv = this.children[this.children.length - 1];
          iconDiv.style.display = 'none';
        });

        messageIconsDiv.addEventListener('click', function () {
          const scope = angular.element(document.getElementById('topbar')).scope();
          const messageInput = document.getElementById('chatline');

          let username = '';

          if (!isOwnMessage) {
            username = chatMessageDiv.previousElementSibling.textContent.slice(0, -1);
          } else {
            username = chatMessageDiv.previousElementSibling.textContent;
          }

          if (scope.chatline === undefined) {
            scope.chatline = '';
          }

          if (!isOwnMessage) {
            scope.chatline += ` @${username} "${messageContent}" `;
          } else {
            scope.chatline += ` @${username} "${messageContent.split(': ')[1]}" `;
          }

          messageInput.focus();
        });

        // View Images in chat
        const imageUrlRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|svg))`);
        const isImageUrl = messageContent.match(imageUrlRegex);

        if (isImageUrl) {
          const imageUrl = isImageUrl[0];
          const imageDiv = document.createElement('div');
          const imageElement = document.createElement('img');

          imageDiv.setAttribute('class', 'message-image-container');
          imageElement.setAttribute('class', 'message-image');

          imageElement.src = imageUrl;

          imageDiv.appendChild(imageElement);

          chatMessageDiv.appendChild(imageDiv);
        }
      }
    }
  };

  const messagesObserver = new MutationObserver(messagesMutationCallback);

  messagesObserver.observe(messageDiv, messageObserverConfig);

  /************************************
   *
   *
   * Avatar-Collections
   * I may have developed a severe depression while trying to implementing this.
   *
   ************************************/

  if (!localStorage.hasOwnProperty('collections')) {
    localStorage.setItem('collections', JSON.stringify({ 'Keine Kategorie': { isDefault: true } }));
  }
  if (!localStorage.hasOwnProperty('avatars')) {
    localStorage.setItem('avatars', JSON.stringify({}));
  }

  window.addEventListener('globalSocketReady', () => {
    window.socket.on('getAvatarcase', (data) => {
      const avatars = data.avatars;

      const storageAvatars = JSON.parse(localStorage.getItem('avatars'));

      for (const avatar of avatars) {
        if (!storageAvatars[avatar.avatarid]) {
          storageAvatars[avatar.avatarid] = {
            id: avatar.avatarid,
            img: avatar.img,
            imgthumb: avatar.imgthumb,
            collection: 'Keine Kategorie',
          };
        }
      }

      localStorage.setItem('avatars', JSON.stringify(storageAvatars));
    });
    window.socket.emit('getAvatarcase');
  });

  window.addEventListener('socketEmit', (event) => {
    if (event.detail.eventName === 'deleteAvatar') {
      // Remove Avatar properly in case it gets deleted
      const avatarID = event.detail.args[0].avatarid;
      const avatarDiv = document.querySelectorAll(`[id^='${avatarID}']`)[0];
      avatarDiv.remove();

      const storageAvatars = JSON.parse(localStorage.getItem('avatars'));

      delete storageAvatars[avatarID];

      localStorage.setItem('avatars', JSON.stringify(storageAvatars));
    }
  });

  window.addAvatarToCollection = function (avatarID, collection) {
    const collections = JSON.parse(localStorage.getItem('collections'));
    if (!collections[collection]) {
      console.log(`Collection '${collection}' doesn't exist!`);
      return false;
    }
    const storageAvatars = JSON.parse(localStorage.getItem('avatars'));

    if (!storageAvatars[avatarID]) {
      console.log(`Avatar '${avatarID}' doesn't exist!`);
      return false;
    }

    storageAvatars[avatarID].collection = collection;
    localStorage.setItem('avatars', JSON.stringify(storageAvatars));
    return true;
  };

  window.removeAvatarFromCollection = function (avatarID) {
    const storageAvatars = JSON.parse(localStorage.getItem('avatars'));
    if (!storageAvatars[avatarID]) {
      console.log(`Avatar '${avatarID}' doesn't exist!`);
      return false;
    }
    storageAvatars[avatarID].collection = 'Keine Kategorie';
    localStorage.setItem('avatars', JSON.stringify(storageAvatars));
    return true;
  };

  window.addCollection = function (collection) {
    const collections = JSON.parse(localStorage.getItem('collections'));
    if (collections[collection]) {
      console.log(`Collection ${collection} already exists!`);
      return false;
    }
    collections[collection] = {
      isDefault: false,
    };

    localStorage.setItem('collections', JSON.stringify(collections));
    return true;
  };

  window.deleteCollection = function (collection) {
    const collections = JSON.parse(localStorage.getItem('collections'));
    if (!collections[collection]) {
      console.log(`Collection '${collection}' doesn't exist!`);
      return false;
    }
    if (collections[collection].isDefault) {
      console.log(`Default collections such as '${collection}' must not be deleted!`);
      return false;
    }

    const storageAvatars = JSON.parse(localStorage.getItem('avatars'));

    for (const avatar in storageAvatars) {
      if (storageAvatars[avatar].collection === collection) {
        storageAvatars[avatar].collection = 'Keine Kategorie';
      }
    }

    localStorage.setItem('avatars', JSON.stringify(storageAvatars));

    delete collections[collection];

    localStorage.setItem('collections', JSON.stringify(collections));
    return true;
  };

  // View

  const avatarcaseObserverConfig = { childList: true };
  const avatarcaseDiv = document.querySelector('[ng-hide="disconnectedClient"]').children[1];

  const avatarcaseMutationCallback = function (mutationsList) {
    for (const mutation of mutationsList) {
      for (const node of mutation.addedNodes) {
        if (node.id === 'avatarcase') {
          const acSelfAvatarElements = document.querySelectorAll('[ng-repeat="ownedAva in ownedAvas track by $index"]');

          setTimeout(() => {
            for (const element of acSelfAvatarElements) {
              element.remove();
            }
          }, 1200);

          const collections = JSON.parse(localStorage.getItem('collections'));
          const avatarCaseDiv = document.getElementsByClassName('avatarcase_main')[0];

          for (const collection in collections) {
            const collapsibleWrap = createCategoryElement(collection, node, avatarCaseDiv);
            avatarCaseDiv.append(collapsibleWrap);
          }

          const newCollectionFormDiv = document.createElement('div');
          const newCollectionInput = document.createElement('input');
          const newCollectionSubmitButton = document.createElement('button');

          newCollectionSubmitButton.textContent = 'Neue Kategorie erstellen';
          newCollectionSubmitButton.id = 'createCollectionBtn';
          newCollectionInput.id = 'createCollectionInput';

          newCollectionFormDiv.appendChild(newCollectionInput);
          newCollectionFormDiv.appendChild(newCollectionSubmitButton);

          newCollectionSubmitButton.addEventListener('click', function () {
            const input = document.getElementById('createCollectionInput');
            if (input.value !== '') {
              const successfullyAdded = window.addCollection(input.value);

              if (successfullyAdded) {
                const collapsbileWrap = createCategoryElement(input.value, node);
                avatarCaseDiv.insertBefore(collapsbileWrap, newCollectionFormDiv);
              }
            }
          });

          avatarCaseDiv.appendChild(newCollectionFormDiv);
        }
      }
    }
  };

  function createCategoryElement(collection, node) {
    const scope = angular.element(document.getElementById('topbar')).scope();

    const collapsibleWrap = document.createElement('div');
    const collapsibleButton = document.createElement('button');
    const collapsibleContent = document.createElement('div');
    const avatarGrid = document.createElement('div');
    const removeCategoryButton = document.createElement('button');

    collapsibleWrap.setAttribute('class', 'collapsible-wrap');
    collapsibleWrap.setAttribute('id', collection);
    collapsibleButton.setAttribute('class', 'collapsible-button');
    collapsibleContent.setAttribute('class', 'collapsible-content');
    removeCategoryButton.setAttribute('class', 'removeCategory-btn');
    avatarGrid.setAttribute('class', 'avatar-grid');
    collapsibleButton.textContent = collection;
    removeCategoryButton.textContent = 'Kategorie löschen';

    removeCategoryButton.addEventListener('click', function () {
      window.selectedCollection = this.parentElement.parentElement.id;

      const requestDeleteCollectionDiv = document.createElement('div');
      const cancelRequestBtn = document.createElement('button');
      const acceptRequestBtn = document.createElement('button');

      requestDeleteCollectionDiv.setAttribute('class', 'request-delete-collection');

      requestDeleteCollectionDiv.textContent = `Kategorie "${window.selectedCollection}" wirklich löschen?`;
      acceptRequestBtn.textContent = 'löschen';
      cancelRequestBtn.textContent = 'abbrechen';

      acceptRequestBtn.addEventListener('click', function () {
        const deletionSuccessfull = window.deleteCollection(window.selectedCollection);

        if (deletionSuccessfull) {
          const defaultCategory = document.getElementById('Keine Kategorie');
          const selecetedCategory = document.getElementById(window.selectedCollection);

          for (const avatar of selecetedCategory.children[1].children[0].children) {
            avatar.children[avatar.childElementCount - 1].remove();
            createAddToCollectionIcon(avatar, node);
            defaultCategory.children[1].children[0].appendChild(avatar);
          }

          selecetedCategory.remove();
        }
        this.parentElement.remove();
      });

      cancelRequestBtn.addEventListener('click', function () {
        this.parentElement.remove();
      });

      requestDeleteCollectionDiv.appendChild(cancelRequestBtn);
      requestDeleteCollectionDiv.appendChild(acceptRequestBtn);

      node.appendChild(requestDeleteCollectionDiv);
    });

    collapsibleContent.appendChild(avatarGrid);

    if (collection !== 'Keine Kategorie') {
      collapsibleContent.appendChild(removeCategoryButton);
    }

    collapsibleWrap.appendChild(collapsibleButton);
    collapsibleWrap.appendChild(collapsibleContent);

    const avatars = JSON.parse(localStorage.getItem('avatars'));

    for (const avatar in avatars) {
      if (avatars[avatar].collection === collection) {
        const avatarDiv = document.createElement('div');
        const deleteIconDiv = document.createElement('div');
        const selectIconDiv = document.createElement('div');

        avatarDiv.id = `${avatars[avatar].id}-${avatars[avatar].img}`;
        avatarDiv.setAttribute('class', 'avatar-div');
        avatarDiv.style.backgroundImage = `url('/img/publicimg/skinthumbnails/${avatars[avatar].imgthumb}')`;

        deleteIconDiv.setAttribute('class', 'avatarIcon');
        selectIconDiv.setAttribute('class', 'avatarIcon');

        deleteIconDiv.innerHTML =
          '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Trash</title><path d="M296 64h-80a7.91 7.91 0 00-8 8v24h96V72a7.91 7.91 0 00-8-8z" fill="none"/><path d="M432 96h-96V72a40 40 0 00-40-40h-80a40 40 0 00-40 40v24H80a16 16 0 000 32h17l19 304.92c1.42 26.85 22 47.08 48 47.08h184c26.13 0 46.3-19.78 48-47l19-305h17a16 16 0 000-32zM192.57 416H192a16 16 0 01-16-15.43l-8-224a16 16 0 1132-1.14l8 224A16 16 0 01192.57 416zM272 400a16 16 0 01-32 0V176a16 16 0 0132 0zm32-304h-96V72a7.91 7.91 0 018-8h80a7.91 7.91 0 018 8zm32 304.57A16 16 0 01320 416h-.58A16 16 0 01304 399.43l8-224a16 16 0 1132 1.14z"/></svg>';

        selectIconDiv.innerHTML =
          '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Shirt</title><path d="M256 96c33.08 0 60.71-25.78 64-58 .3-3-3-6-6-6a13 13 0 00-4.74.9c-.2.08-21.1 8.1-53.26 8.1s-53.1-8-53.26-8.1a16.21 16.21 0 00-5.3-.9h-.06a5.69 5.69 0 00-5.38 6c3.35 32.16 31 58 64 58z"/><path d="M485.29 89.9L356 44.64a4 4 0 00-5.27 3.16 96 96 0 01-189.38 0 4 4 0 00-5.35-3.16L26.71 89.9A16 16 0 0016.28 108l16.63 88a16 16 0 0013.92 12.9l48.88 5.52a8 8 0 017.1 8.19l-7.33 240.9a16 16 0 009.1 14.94A17.49 17.49 0 00112 480h288a17.49 17.49 0 007.42-1.55 16 16 0 009.1-14.94l-7.33-240.9a8 8 0 017.1-8.19l48.88-5.52a16 16 0 0013.92-12.9l16.63-88a16 16 0 00-10.43-18.1z"/></svg>';

        avatarDiv.appendChild(deleteIconDiv);
        avatarDiv.appendChild(selectIconDiv);

        selectIconDiv.addEventListener('click', function () {
          scope.changeAvatarTexture(this.parentElement.id.split('-')[1]);
        });

        deleteIconDiv.addEventListener('click', function () {
          scope.requestDeleteAvatar({ avatarid: this.parentElement.id.split('-')[0] });
        });

        if (collection === 'Keine Kategorie') {
          createAddToCollectionIcon(avatarDiv, node);
        } else {
          createRemoveFromCollectionIcon(avatarDiv, node);
        }

        avatarDiv.addEventListener('mouseover', function () {
          for (const child of this.children) {
            child.style.display = 'flex';
          }
        });

        avatarDiv.addEventListener('mouseout', function () {
          for (const child of this.children) {
            child.style.display = 'none';
          }
        });

        avatarGrid.appendChild(avatarDiv);
      }
    }

    collapsibleButton.addEventListener('click', function () {
      this.classList.toggle('active');
      const content = this.nextElementSibling;

      if (content.style.maxHeight) {
        content.style.maxHeight = null;
      } else {
        content.style.maxHeight = content.scrollHeight + 'px';
      }
    });

    return collapsibleWrap;
  }

  function createAddToCollectionIcon(avatarDiv, node) {
    const addToCollectionIcon = document.createElement('div');

    addToCollectionIcon.setAttribute('class', 'avatarIcon');
    addToCollectionIcon.innerHTML =
      '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Bag Add</title><path d="M454.66 169.4A31.86 31.86 0 00432 160h-64v-16a112 112 0 00-224 0v16H80a32 32 0 00-32 32v216c0 39 33 72 72 72h272a72.22 72.22 0 0050.48-20.55 69.48 69.48 0 0021.52-50.2V192a31.78 31.78 0 00-9.34-22.6zM320 336h-48v48a16 16 0 01-32 0v-48h-48a16 16 0 010-32h48v-48a16 16 0 0132 0v48h48a16 16 0 010 32zm16-176H176v-16a80 80 0 01160 0z"/></svg>';

    addToCollectionIcon.addEventListener('click', function () {
      const collections = JSON.parse(localStorage.getItem('collections'));
      window.selectedAvatar = this.parentElement.id.split('-')[0];

      const collectionSelection = document.createElement('div');
      const cancelSelectionBtn = document.createElement('button');

      cancelSelectionBtn.textContent = 'abbrechen';

      collectionSelection.setAttribute('class', 'collection-selection');
      for (const collection in collections) {
        const collectionSelectable = document.createElement('div');
        collectionSelectable.setAttribute('class', 'collection-selectable');
        collectionSelectable.textContent = collection;
        collectionSelection.appendChild(collectionSelectable);
        collectionSelectable.addEventListener('click', function () {
          window.addAvatarToCollection(window.selectedAvatar, this.textContent);
          const avatarDiv = document.querySelectorAll(`[id^='${window.selectedAvatar}']`)[0];
          const newCollectionDiv = document.getElementById(this.textContent).children[1].children[0];

          avatarDiv.children[avatarDiv.childElementCount - 1].remove();

          createRemoveFromCollectionIcon(avatarDiv, node);

          for (const child of avatarDiv.children) {
            child.style.display = 'none';
          }

          newCollectionDiv.appendChild(avatarDiv);

          this.parentElement.remove();
        });
      }

      cancelSelectionBtn.addEventListener('click', function () {
        this.parentElement.remove();
      });

      collectionSelection.appendChild(cancelSelectionBtn);
      node.appendChild(collectionSelection);
    });

    avatarDiv.appendChild(addToCollectionIcon);
  }

  function createRemoveFromCollectionIcon(avatarDiv, node) {
    const removeFromCollectionIcon = document.createElement('div');
    removeFromCollectionIcon.setAttribute('class', 'avatarIcon');
    removeFromCollectionIcon.innerHTML =
      '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Bag Remove</title><path d="M454.66 169.4A31.86 31.86 0 00432 160h-64v-16a112 112 0 00-224 0v16H80a32 32 0 00-32 32v216c0 39 33 72 72 72h272a72.22 72.22 0 0050.48-20.55 69.48 69.48 0 0021.52-50.2V192a31.78 31.78 0 00-9.34-22.6zM320 336H192a16 16 0 010-32h128a16 16 0 010 32zm16-176H176v-16a80 80 0 01160 0z"/></svg>';

    removeFromCollectionIcon.addEventListener('click', function () {
      const avatarDiv = this.parentElement;
      const newCollectionDiv = document.getElementById('Keine Kategorie').children[1].children[0];

      avatarDiv.children[avatarDiv.childElementCount - 1].remove();

      createAddToCollectionIcon(avatarDiv, node);

      for (const child of avatarDiv.children) {
        child.style.display = 'none';
      }

      window.removeAvatarFromCollection(avatarDiv.id.split('-')[0]);
      newCollectionDiv.appendChild(avatarDiv);
    });

    avatarDiv.appendChild(removeFromCollectionIcon);
  }

  const avatarcaseObserver = new MutationObserver(avatarcaseMutationCallback);

  avatarcaseObserver.observe(avatarcaseDiv, avatarcaseObserverConfig);
})();