pages/dashboard/index.js

/**
 * Functions directly available from the renderer of Traktify's dashboard page.
 * @namespace dashboard
 */


const trakt = remote.getGlobal('trakt')
const fanart = remote.getGlobal('fanart')
const getSettings = remote.getGlobal('getSettings')
const setSetting = remote.getGlobal('setSetting')
const defaultAll = remote.getGlobal('defaultAll')
const updateApp = remote.getGlobal('updateApp')
const relaunchApp = remote.getGlobal('relaunchApp')

let config = remote.getGlobal('config')

const generate = require('./../../modules/components/generators.js')
const posters = require('./../../modules/api/posters.js')


// Here we update the app with saved settings after the window is created
window.onload = function() {
  debugLog('window', 'dashboard loading')
  updateApp() // update settings
  generatePosterSection() // show the up next to watch posters
  updateRpc() // show rpc on discord, handling the on/off setting is done within this function and doesn't have to be done here!
}

// This guy waits for messages on the 'modify-root' channel. The messages contain setting objects that then get applied to the 'master.css' style sheet. It is used to change the look of the app.
ipcRenderer.on('modify-root', (event, data) => {
  let variables = document.styleSheets[0]
    .cssRules[0].style.cssText.split(';')

  let result = {}
  for(let i in variables) {
    let a = variables[i].split(':')
    if(a[0] !== '') {
      result[a[0].trim()] = a[1].trim()
    }
  }

  let keys = Object.keys(result)
  document.documentElement.style.setProperty(keys[keys.indexOf(data.name)], data.value)
})

// Identifies the currently open panel. Makes it easier to check against closed panels, as this would require each of them to check on their own.
let openedPanel = null
/**
 * sidebar,
 * cards
 */

// Here, dashboard-wide shortcuts are defined. The 'meta' key represents CMD on macOS and Ctrl on Windows
document.onkeydown = function() {
  if(event.metaKey && event.keyCode === 83) { // meta + S
    debugLog('shortcut', 'Meta + S')
    if(openedPanel !== 'cards') {
      show(document.getElementById('search_button_side'))
      triggerSidePanel('search')
    }
    return false
  } else if(event.metaKey && event.keyCode === 188) { // meta + ,
    debugLog('shortcut', 'Meta + ,')
    if(openedPanel !== 'cards') {
      show(document.getElementById('settings_button_side'))
      triggerSidePanel('settings')
    }
    return false
  } else if(event.keyCode === 27) { // ESC
    debugLog('shortcut', 'ESC')
    // close the currently open panel
    if(openedPanel == 'sidebar') {
      triggerSidePanel(sideBar.status)
    } else if(openedPanel == 'cards') {
      triggerInfoCardOverlay()
    }
  } else if(event.keyCode === 39) { // arrow right
    debugLog('shortcut', 'ArrowRight')
    if(openedPanel == 'cards') {
      moveCards('right')
    }
  } else if(event.keyCode === 37) { // arrow left
    debugLog('shortcut', 'ArrowLeft')
    if(openedPanel == 'cards') {
      moveCards('left')
    }
  } else if(event.keyCode === 38) { // arrow up

  } else if(event.keyCode === 40) { // arrow down

  }
}


//:::: INFOCARD ::::\\

// This variable can be overwritten by different new <>Buffer() classes.
let localBuffer

// moves in one direction through the stacks
function moveCards(direction) {
  let stacks = getCardStacks()
  switch(direction) {
    case 'right':
      if(stacks.right.length !== 0) {
        let midCard = stacks.middle[0]
        midCard.classList.remove('middle_stack')
        midCard.classList.add('left_stack')
        // get the bottom right one
        let rigCard = stacks.right[0]
        rigCard.classList.remove('right_stack')
        rigCard.classList.add('middle_stack')
      }

      localBuffer.move(1, {
        first: epData => { // onFirst
          // find index of the middle card
          let index = getCardStacks().left.length
          updateInfoCard(epData, index)
          updateLeftRightButtons()
        },
        buffer: (bufferData, pos) => { // onBuffer
          updateInfoCard(bufferData, pos)
        },
        images: (urls, pos) => { // onImage
          updateInfoCardImage(urls, pos)
        }
      })
      break
    case 'left':
      if(stacks.left.length !== 0) {
        // move the middle one
        let midCard = stacks.middle[0]
        midCard.classList.remove('middle_stack')
        midCard.classList.add('right_stack')
        // get the top left one
        let lefCard = stacks.left[stacks.left.length-1]
        lefCard.classList.remove('left_stack')
        lefCard.classList.add('middle_stack')
      }

      localBuffer.move(-1, {
        first: epData => { // onFirst
          // find index of the middle card
          let index = getCardStacks().left.length
          updateInfoCard(epData, index)
          updateLeftRightButtons()
        },
        buffer: (bufferData, pos) => { // onBuffer
          updateInfoCard(bufferData, pos)
        },
        images: (urls, pos) => { // onImage
          updateInfoCardImage(urls, pos)
        }
      })
      break
  }
  updateLeftRightButtons()
}

function getCardStacks() {
  return {
    left: document.getElementsByClassName('left_stack'),
    middle: document.getElementsByClassName('middle_stack'),
    right: document.getElementsByClassName('right_stack')
  }
}

function updateLeftRightButtons() {
  let stacks = getCardStacks()
  let leftButton = document.getElementById('stack_left_button')
  let rightButton = document.getElementById('stack_right_button')

  // check the left stack
  if(stacks.left.length === 0) {
    leftButton.style.display = 'none'
  } else {
    leftButton.style.display = 'flex'
  }

  // and now the right one
  if(stacks.right.length === 0) {
    rightButton.style.display = 'none'
  } else {
    rightButton.style.display = 'flex'
  }

  // update the position of the slider thumb
  generateStackSlider()
}

// Closes the info card if already open. Currently, it is only opened by html onclick events. Opening it with this function could be done in future, possibly to speed up loading.
function triggerInfoCardOverlay() {
  let infocard_overlay = document.getElementById('infocard_overlay')
  if(infocard_overlay.style.display === 'none') { // open it
    triggerOverlay('info_overlay')
    openedPanel = 'cards'
    infocard_overlay.style.display = 'flex'
  } else { // close it
    triggerOverlay('info_overlay')
    openedPanel = null
    infocard_overlay.style.display = 'none'
    document.getElementById('infocard_stack').innerHTML = ''

    // Here, we could nullize the localBuffer so it is not falsely used by some other instance. When doing so, the whole instance would have to be initiated again when reopening the stacks. Because the user could reopen the same card-stack after closing without opening a different item before, we could instead keep the created instance and only overwrite the localBuffer when the opened item is not the same as before.
  }
}


function addInfoCard(position, index, traktId) {
  let stack
  switch(position) {
    case 'left':
      stack = 'left_stack'
      break
    case 'middle':
      stack = 'middle_stack'
      break
    case 'right':
      stack = 'right_stack'
      break
  }
  let infocard_stack = document.getElementById('infocard_stack')
  infocard_stack.appendChild(generate.infoCardDummy(stack, index, traktId))
}

function generateStackSlider() {
  let slider = document.getElementById('indicator_slider')
  let stacks = getCardStacks()
  let totalSize = stacks.left.length
    + stacks.middle.length // will always be 1, this makes it understandable
    + stacks.right.length

  // Here, we could check if there are more than one items, but its okay to show a single red bar for now.
  // set the width of the thump to match the ratio
  let sliderWidth = slider.offsetWidth/totalSize
  if(sliderWidth < 15) {
    sliderWidth = 15 // fix width to height, so it stays visible
  }

  let styler = document.querySelector('[data="indicator"]')
  styler.innerHTML = `
    #indicator input::-webkit-slider-thumb {
      width: ${sliderWidth}px !important;
    }
  `
  // set position of the thumb
  slider.min = 1;
  slider.max = totalSize
  slider.value = stacks.left.length+1
}

function updateInfoCard(itemUpdates, index) {
  let stacks = getCardStacks()
  debugLog('updating card', index)

  let theStack

  if(index < stacks.left.length) {
    theStack = stacks.left[index]
  } else if(index == stacks.left.length) {
    theStack = stacks.middle[0]
  } else {
    theStack = stacks.right[index - stacks.left.length-1]
  }

  if(theStack !== undefined) {
    theStack.innerHTML = generate.infoCardContent(itemUpdates)
  } else {
    debugLog('!updating card', 'failed, could not find element')
  }
}

function updateInfoCardImage(url, index) {
  let stacks = getCardStacks()
  debugLog('updating card images', index)

  let theCard

  if(index < stacks.left.length) {
    theCard = stacks.left[index]
  } else if(index == stacks.left.length) {
    theCard = stacks.middle[0]    
  } else {
    theCard = stacks.right[index - stacks.left.length-1]
  }

  /** url:
   *    banner
   *    poster
   *    actors[]
   */
  if(theCard !== undefined) {
    theCard.querySelector('.infocard_left').children[0].src = url.banner
    theCard.querySelector('.infocard_left').children[1].src = url.poster
  } else {
    debugLog('!updating card', 'failed, could not find element')
  }
}


/*:::: SIDE-BAR ::::*/

// This object holds the DOM-elements and actions of the sidebar. We need this to generate the frame of the sidebar where content can be added dynamically later. Further comments explain the functioning.
let sideBar = {
  element: document.getElementById('side_panel'),
  // This variable tells which sidebar is currently open. The possible values are:
  // 'none' | 'search' | 'settings' | 'logout'
  status: 'none',
  // These are the available panels
  panels: ['search', 'settings', 'logout'],
  // Now these are the panel creators
  search: {
    create: function() {
      let panel = document.createElement('div')
      panel.classList.add('panel')

      let search_field = document.createElement('input')
      search_field.classList.add('panel_header', 'search', 'fs18', 'fw500', 'white_t', 'black_d_b', 'z4')
      search_field.type = 'search'
      search_field.onkeydown = function() {
        if(event.keyCode === 13) { // ENTER
          search(search_field.value)
          return false
        }
      }

      setTimeout(() => {
        search_field.focus()
      }, 220)
      panel.appendChild(search_field)

      let box = document.createElement('div')
      box.classList.add('panel_header_box', 'top', 'z3')
      panel.appendChild(box)

      let gradient = document.createElement('div')
      gradient.classList.add('panel_header_gradient', 'top_p', 'z3')
      panel.appendChild(gradient)

      let results = document.createElement('div')
      results.classList.add('side_panel_list')
      results.id = 'results'
      panel.appendChild(results)

      return panel
    },
    open: function() {
      this.parent.appendChild(this.create())
    }
  },
  settings: {
    create: function() {
      let panel = document.createElement('div')
      panel.classList.add('panel')

      let headText = document.createElement('h2')
      headText.classList.add('panel_header', 'fs23', 'fw500', 'white_t', 'z4')
      headText.innerText = 'Settings'
      panel.appendChild(headText)

      let box = document.createElement('div')
      box.classList.add('panel_header_box', 'top', 'z3')
      panel.appendChild(box)

      let gradient = document.createElement('div')
      gradient.classList.add('panel_header_gradient', 'top_p', 'z3')
      panel.appendChild(gradient)

      let setting_list = document.createElement('div')
      setting_list.classList.add('side_panel_list', 'animation_slide_right')

      let settings = getSettings('app')

      let settingsArray = objectToArray(settings)

      delayFunction((index, arr) => {
        let s = arr[index].name
        let settingBox = addSetting(settings[s], s)
        setting_list.appendChild(settingBox)
      }, 150, getObjectLength(settings), settingsArray, 2)

      let relaunch_box = document.createElement('div')
      relaunch_box.id = 'relaunch_box'
      relaunch_box.innerHTML = `<h3 class="fs18 fw500 white_t">Some settings require a</h3>` // rest is added below
      relaunch_box.classList.add('black_d_b', 'shadow_h', 'bottom', 'z4')
      relaunch_box.style.visibility = 'hidden'

      let relaunch_button = document.createElement('div')
      relaunch_button.innerText = 'relaunch'
      relaunch_button.classList.add('btn', 'red_d_b', 'white_t')
      relaunch_button.onclick = function() {
        relaunchApp()
      }
      relaunch_box.appendChild(relaunch_button)

      panel.appendChild(setting_list)
      panel.appendChild(relaunch_box)
      return panel
    },
    open: function() {
      this.parent.appendChild(this.create())
    }
  },
  logout: {
    create: function() {
      let panel = document.createElement('div')
      panel.classList.add('panel')

      let logout_button = document.createElement('button')
      logout_button.classList.add('logout_btn', 'fs18', 'white_t', 'black_d_b')
      logout_button.innerText = 'Logout'
      logout_button.onclick = function() {
        signout()
        return false
      }
      panel.appendChild(logout_button)

      let logout_text = document.createElement('div')
      logout_text.style = 'text-align:center;'
      logout_text.innerHTML = '<p>Oh uh!<br>You really want to do this?</p>'
      panel.appendChild(logout_text)
      return panel
    },
    open: function() {
      this.parent.appendChild(this.create())
    }
  },
  // Removes the panel contents from the sidebar
  removeAll: function() {
    let panels = this.element.getElementsByClassName('panel')
    while(panels[0]) {
      panels[0].parentNode.removeChild(panels[0])
    }
  },
  // This helper initializes the available panels by providing the sidebar element as a parent. The method is called right after the creation of this object.
  init: function() {
    this.panels.forEach(panel => {
      this[panel].parent = this.element
    })
    delete this.init
    return this
  }
}.init()


// Opens and closes the given panel
function triggerSidePanel(panelName) {
  // Checking if panel is available. This will not be accessible by the user directly, so we could live without the check but for possible future changes it's safer to have and not wonder about weird errors
  if(!sideBar.panels.includes(panelName)) {
    throw 'panel not available'
  }

  let side_buttons = document.getElementById('side_buttons')
  let side_panel = document.getElementById('side_panel')

  if(sideBar.status == 'none') {
    sideBar.status = panelName
    openedPanel = 'sidebar'

    triggerOverlay('sidepanel_overlay')

    // now showing the settings panel
    side_panel.classList.remove('side_panel_animate_out')
    side_panel.classList.add('side_panel_animate_in')
    side_buttons.classList.remove('side_buttons_animate_out')
    side_buttons.classList.add('side_buttons_animate_in')

    sideBar[panelName].open()
  }

  // When the panel-button of the currently opened panel was clicked, the whole sidebar will close
  else if(sideBar.status == panelName) {
    if(sideBar.status == 'search') {
      removeSearchResults()
    }
    sideBar.status = 'none'
    openedPanel = null

    // removing the settings panel
    side_panel.classList.remove('side_panel_animate_in')
    side_panel.classList.add('side_panel_animate_out')
    side_buttons.classList.remove('side_buttons_animate_in')
    side_buttons.classList.add('side_buttons_animate_out')

    triggerOverlay('sidepanel_overlay', sideBar.removeAll())
    show(document.getElementById('search_button_side'))
  }

  // When another button than the currently open panel was clicked, the sidebar stays open and changes it's content
  else {
    sideBar.status = panelName
    sideBar.removeAll()
    sideBar[panelName].open()
  }
}

// These functions are called by onclicks in the HTML
function openSearch() {
  triggerSidePanel('search')
}

function openSettings() {
  triggerSidePanel('settings')
}

function openLogout() {
  triggerSidePanel('logout')
}

function closeSidePanel() {
  try {
    triggerSidePanel(sideBar.status)
  } catch(err) {
    debugLog('error', err)
  }
}

function triggerOverlay(id, cb) {
  let overlay = document.getElementById(id)
  
  if(overlay.getAttribute('data-opened') == 'true') {
    overlay.classList.add('hide')
    overlay.setAttribute('data-opened', 'false')
    overlay.style.display = 'none'
  }else if(overlay.getAttribute('data-opened') == 'false') {
    overlay.classList.remove('hide')
    overlay.style.display = 'block'
    overlay.setAttribute('data-opened', 'true')

    setTimeout(() => {
      overlay.style.display = 'none'
      if(cb) {
        cb()
      }
    }, 200)
  }
}


/*:::: SETTINGS PANEL ::::*/
let wantsRelaunch = []

// This adds a setting box to the sidepanel
function addSetting(setting, name) {
  let setting_area = document.createElement('div')
  setting_area.classList.add('setting_holder')
  
  let setting_title = document.createElement('h3')
  setting_title.classList.add('fs18', 'fw500', 'tu', 'tOverflow')
  setting_title.innerText = name

  let settingOld = setting.status

  function alertRequiredReload(settingNew) {
    let relaunch_box = document.getElementById('relaunch_box')
    let setting_list = document.getElementById('side_panel')
    let panel = setting_list.children[0]

    if(settingNew !== settingOld) {
      wantsRelaunch.push(name)
      relaunch_box.style = 'visiblity:visible;'
      relaunch_box.classList.remove('animation_fade_out')
      relaunch_box.classList.add('animation_slide_up')
      panel.children[3].classList.add('relaunch')
      let pos = panel.scrollTop
      panel.scrollTop = pos+200
    } else {
      wantsRelaunch = wantsRelaunch.filter(item => item !== name)
      if(wantsRelaunch.length === 0) {
        relaunch_box.classList.remove('animation_slide_up')
        relaunch_box.classList.add('animation_fade_out')
        panel.children[3].classList.remove('relaunch')
      }
    }
    debugLog('relaunch required', wantsRelaunch)
  }  

  switch(setting.type) {
    case 'select': {
      classname = 'setting_select'

      for(let o in setting.options) {
        let opt = setting.options[o]

        let setting_contain = document.createElement('div')
        setting_contain.classList.add('setting_container')

        let preview = document.createElement('div')
        preview.classList.add('setting_box')

        let def = document.createElement('div')
        def.classList.add('setting_def', 'fs14' , 'white_d_t', 'tu', 'tOverflow')

        if(setting.default == o) {
          def.innerText = 'default'
        }

        if(setting.status == o) {
          preview.classList.add('selected')
        }

        preview.onclick = function() {
          if(!preview.classList.contains('selected')) {
            let par = preview.parentElement.parentElement;
            [...par.children].forEach(element => {
              if(element.children[0] == preview) {
                preview.classList.add('selected')
                setSetting('app', name, o)
                updateApp()
              } else {
                element.children[0].classList.remove('selected')
              }
            })
          }
        }

        if(opt.preview) {
          def.classList.add('top')
          setting_contain.classList.add('wide')
          preview.style.backgroundImage = `url('../../assets/previews/${opt.value}')`
        } else {
          setting_area.style = 'display:flex;justify-content:space-between;'
          preview.style.backgroundColor = opt.value
        }

        setting_contain.appendChild(preview)
        setting_contain.appendChild(def)
        setting_area.appendChild(setting_contain)
      }
      break
    }
    case 'toggle': {
      classname = 'setting_toggle'
      let check_no = ''
      let check_yes = ''
      if(setting.status) {
        check_yes = 'checked'
      } else {
        check_no = 'checked'
      }

      let idname = name.split(' ').join('_')

      let toggle_switch = document.createElement('div')
      toggle_switch.innerHTML = `
      <p class="btn-switch" id="setting_${idname}">
        <input ${check_no} type="radio" id="no_${idname}" name="switch_${idname}" class="btn-switch__radio btn-switch__radio_no"/>
        <input ${check_yes} type="radio" id="yes_${idname}" name="switch_${idname}" class="btn-switch__radio btn-switch__radio_yes"/>
        <label for="no_${idname}" class="btn-switch__label btn-switch__label_no">
          <span class="btn-switch__txt"></span>
        </label>
        <label for="yes_${idname}" class="btn-switch__label btn-switch__label_yes">
          <span class="btn-switch__txt"></span>
        </label>
      </p>
      `

      toggle_switch.onclick = function() {
        let radio = document.getElementById(`yes_${idname}`)
        alertRequiredReload(radio.checked)
        setSetting('app', name, radio.checked)
        updateApp()
      }

      let def = document.createElement('div')
      def.classList.add('setting_def', 'fs14' , 'white_d_t', 'tu')

      def.innerText = 'default: '
      if(setting.default) {
        def.innerText += 'on'
      } else {
        def.innerText += 'off'
      }

      setting_area.style = 'display:flex;justify-content:space-between;align-items:center;'
      setting_area.appendChild(toggle_switch)
      setting_area.appendChild(def)
      break
    }
    case 'range': {
      classname = 'setting_range'

      let slider = document.createElement('input')
      slider.type = 'range'
      slider.min = setting.range[0] / setting.accuracy
      slider.max = setting.range[1] / setting.accuracy
      slider.value = setting.status / setting.accuracy
      slider.style.background = `linear-gradient(to right, var(--accent_color) 0%, var(--accent_color) ${setting.status}%, var(--white_d) ${setting.status}%, var(--white_d) 100%)`;
      slider.classList.add('slider')

      slider.oninput = function() {
        let value = slider.value * setting.accuracy
        slider.style.background = 'linear-gradient(to right, var(--accent_color) 0%, var(--accent_color) '+value +'%, var(--white_d) ' + value + '%, var(--white_d) 100%)'
        setSetting('app', name, value)
        updateApp()
      }

      let def = document.createElement('div')
      def.classList.add('setting_def', 'fs14' , 'white_d_t', 'tu')

      def.innerText = 'default: ' + setting.default

      setting_area.appendChild(slider)
      setting_area.appendChild(def)
      break
    }
    default: { break }
  }

  let box = document.createElement('div')
  box.classList.add('panel_box', 'panel_box_container', 'setting')

  box.appendChild(setting_title)
  box.appendChild(setting_area)
  return box
}

/*:::: SEARCH-PANEL ::::*/
let searchHistoryCache = new Cache('searchHistory')

// This gets fired when the user searches something from the sidebar
async function search(text) {
  let requestTime = Date.now()
  removeSearchResults()

  if(text == '') {
    // empty search submitted
    return false
  }

  let data = await searchRequestHelper(text).then(res => res)

  data.result.forEach(item => {
    let type = item.trakt.type

    debugLog('search', `adding result ${item.trakt[type].ids.trakt} (${item.trakt.score})`)
    // fallback for unavailable images
    let img = url = '../../assets/'+config.client.placeholder.search

    if(item.fanart !== undefined) {
      if(item.fanart.hasOwnProperty('tvposter')) {
        img = item.fanart.tvposter[0].url
      } else if(item.fanart.hasOwnProperty('movieposter')) {
        img = item.fanart.movieposter[0].url
      }
    }

    // render search result
    let panel = document.getElementById('results')
    
    let result = generate.searchResult({
      title: item.trakt[type].title,
      type: type,
      rating: Math.round(item.trakt[type].rating * 10),
      img: img,
      description: item.trakt[type].overview,
      id: item.trakt[type].ids.tmdb
    })

    panel.appendChild(result)
  })

  debugLog('time taken', Date.now()-requestTime+'ms')
}


// Removes all elements from the search panel in the sidebar
function removeSearchResults() {
  let panel = document.getElementById('results')
  boxes = panel.getElementsByClassName('panel_box search')
  while(boxes[0]) {
    boxes[0].parentNode.removeChild(boxes[0])
  }
}


/*:::: UP-NEXT-TO-WATCH ::::*/
// This gets fired when the dashboard is loaded
async function generatePosterSection(update) {
  let requestTime = Date.now()

  let data = await getUnfinishedProgressList(5, update)

  if(update) {
    // clear dashboard
    document.querySelector('#dash').innerHTML = `
      <div class="titles" id="poster_title"></div>
      <ul class="posters" id="posters"></ul>`
  }

  data.forEach((item, index) => {
    debugLog('item to add', item.show.show.title)

    let next = item.progress.next_episode
    let title = item.show.show.title
    let subtitle = `${next.season} x ${next.number+(next.number_abs?' ('+next.number_abs +')':'')} ${next.title}`

    if(index === 0) {
      let titleElement = document.getElementById('poster_title')
      titleElement.innerHTML = generate.upNextTitle({
        title,
        subtitle
      })
    }

    let posterSection = document.getElementById('posters')
    let poster = generate.upNextPoster({
      title: title,
      subtitle: subtitle,
      rating: next.rating,
      id: item.show.show.ids.tvdb,
      season: next.season,
      matcher: `${item.show.show.ids.trakt}_e_${next.season}_${next.number}`
    })

    posterSection.appendChild(poster)
  })

  debugLog('time taken',  Date.now()-requestTime+'ms')
}


function animateText(textBox, onenter) {
  if(!textBox.children[0].classList.contains('hidden')){
    let container = document.getElementById('poster_title')
    let container_title = container.children[1]
    let container_subtitle = container.children[2]

    let title = textBox.getAttribute('data_title')
    let subtitle = textBox.getAttribute('data_subtitle')

    if(title.toLowerCase() !== container_title.innerText.toLowerCase()) {
      if(onenter) {
        toggleAnimation(container_title, 'animation_slide_up', title)
        toggleAnimation(container_subtitle, 'animation_slide_up', subtitle)
      }
    }

    let poster = document.getElementById('posters').firstChild
    let poster_title = poster.getAttribute('data_title')
    let poster_subtitle = poster.getAttribute('data_subtitle')

    if(poster_title.toLowerCase() !== container_title.innerText.toLowerCase()) {
      if(!onenter) {
        toggleAnimation(container_title, 'animation_slide_up', poster_title)
        toggleAnimation(container_subtitle, 'animation_slide_up', poster_subtitle)
      }
    }
  }
}

function toggleAnimation(x, y, z) {
  x.classList.remove(y)
  void x.offsetWidth
  x.innerText = z
  x.classList.add(y)
}

/**
 * First function to trigger when a poster is clicked.
 * @param {HTMLElement} poster Image which got clicked on
 */
function openInfoCard(poster) {
  // matcher layout: <show_id>_<m,t,s,e,p,l>_[season]_[episode]
  let matcher = poster.getAttribute('data_matcher')
  debugLog('info card', matcher)
  matcher = matcher.split('_')

  let showId = matcher[0]

  switch(matcher[1]) {
    case 'e': { // episode
      let seasonNum = matcher[2]
      let episodeNum = matcher[3]

      let onCallbacks = {
        size: epPosition => {
          // remove possibly existing dummies that were used as a loading indicator
          document.getElementById('infocard_stack').innerHTML = ''

          let leftStackSize = epPosition.current - 1
          let rightStackSize = epPosition.total - epPosition.current

          let i = 0

          for(i; i<leftStackSize; i++) {
            addInfoCard('left', i, showId)
          }

          addInfoCard('middle', i, showId)

          for(let j=1; j<=rightStackSize; j++) {
            addInfoCard('right', i+j, showId)
          }
          updateLeftRightButtons()
          generateStackSlider()
        },
        first: epData => { // onFirst
          // find index of the middle card
          let index = getCardStacks().left.length
          updateInfoCard(epData, index)
          updateLeftRightButtons()
        },
        buffer: (bufferData, pos) => { // onBuffer
          updateInfoCard(bufferData, pos)
        },
        images: (urls, pos) => { // onImage
          updateInfoCardImage(urls, pos)
        }
      }

      if(localBuffer instanceof showBuffer && localBuffer.id == showId) {
        localBuffer.openAt(seasonNum, episodeNum, onCallbacks)
      } else {
        localBuffer = new showBuffer(showId)
        localBuffer.initAt(seasonNum, episodeNum, onCallbacks)
      }

      break
    }
  }

  triggerInfoCardOverlay()
}

/*:::: RPC ::::*/
async function updateRpc() {
  if(config.client.settings.app['discord rpc'].status) {
    let settings = await createRpcContent()
    let stateArray = config.client.rpc.states
    settings.state = pick(stateArray)
    rpc.update(settings)
    setInterval(() => {
      settings.state = pick(stateArray)
      rpc.update(settings)
    }, 479e3)
  }
}

async function createRpcContent() {
  let stats = await getUserStats() // from request module
  return {
    time: {
      movies: stats.movies.minutes,
      shows: stats.episodes.minutes
    }
  }
}

/*:::: ACTION BUTTONS ::::*/
// functions that get called when clicking on action buttons

function playNow(elm, matcher, modal) {
  showAlertBoxAndWait({
    title: 'Info',
    description: `Will start playing <span>${getItemName(elm, modal)}</span>.`, 
    acceptButtonText: 'OK',
    declineButtonText: 'Revert Action'
  }, proceed => {
    if(proceed) {
      console.log('playing')
      showAlertBoxAndWait()
      // start playing
    } else {
      console.log('declined playing')
      showAlertBoxAndWait()
    }
  })
}

function addToWatchlist(elm, matcher, modal) {
  showAlertBoxAndWait({
    title: 'Info',
    description: `Adding <span>${getItemName(elm, modal)}</span> to watchlist.`, 
    acceptButtonText: 'OK',
    declineButtonText: 'Revert Action'
  }, proceed => {
    if(proceed) {
      console.log('added to watchlist')
      showAlertBoxAndWait()
      // start playing
    } else {
      console.log('declined addition to watchlist')
      showAlertBoxAndWait()
    }
  })
}

function addToHistory(elm, matcher, modal) {
  let [id, type, se, ep] = matcher.split('_')

  showAlertBoxAndWait({
    title: 'Info',
    description: `Adding <span>${getItemName(elm, modal)}</span> to history.`, 
    acceptButtonText: 'OK',
    declineButtonText: 'Revert Action'
  }, proceed => {
    if(proceed) {
      console.log('added to history')
      posters.requestHistoryUpdatePosting(id, {
        type: type == 'e' ? 'episode' : 'movie',
        season: type == 'e' ? Number(se) : null,
        episode: type == 'e' ? Number(ep) : null
      })
      showAlertBoxAndWait()
    } else {
      console.log('declined addition to history')
      showAlertBoxAndWait()
    }
  })
}

function getItemName(elm, modal) {
  let itemName = ''
  if(modal == 'poster') {
    let poster = elm.closest('.poster')
    itemName = poster.getAttribute('data_title') + ' ' +poster.getAttribute('data_subtitle')
  } else if(modal == 'card') {
    itemName = elm.closest('.infocard').getAttribute('data-trakt_id')
  }
  return itemName
}

/**
 * This triggers an alert box where the user can accept or decline his recently performed action. It will be reusable across the whole app.
 * @memberof dashboard
 * @param {Object} options Individual settings for the popup
 * @param {String} options.title
 * @param {String} options.description
 * @param {String} options.acceptButtonText
 * @param {String} options.declineButtonText
 * @param {Function} proceed Callback sending sending back status
 */
function showAlertBoxAndWait(options, proceed) {
  let box = document.getElementById('alertBox')

  triggerOverlay('alert_overlay')

  if(box.style.display == 'none') {
    box.style.display = 'flex'
    box.appendChild(generate.confirmActionAlertBox(options, proceed))
  } else{
    box.style.display = 'none'
    box.innerHTML = ''
  }  
}