DLsite Filter Works by Category for Circle Pages

Add a combox in circle pages to filter works by category

// ==UserScript==
// @name        DLsite Filter Works by Category for Circle Pages
// @namespace   Zero_G@4d7d460c-0424-11eb-adc1-0242ac120002
// @description Add a combox in circle pages to filter works by category
// @include     *.dlsite.com/*/circle/profile/=/*
// @version     1.2
// @grant       none
// @license MIT
// ==/UserScript==

(function () {
  // Define categories
  var categories = [
    {
        '---Games---': ['type_ACN', 'type_QIZ', 'type_ADV', 'type_RPG', 'type_TBL', 'type_DNV', 'type_SLN', 'type_TYP', 'type_TYP', 'type_STG', 'type_PZL', 'type_ETC'],
        'Action': 'type_ACN',
        'Quiz': 'type_QIZ',
        'Adventure': 'type_ADV',
        'RPG': 'type_RPG',
        'Table': 'type_TBL',
        'Digital Novel': 'type_DNV',
        'Simulation': 'type_SLN',
        'Typing': 'type_TYP',
        'Shooting': 'type_STG',
        'Puzzle': 'type_PZL',
        'Miscellaneous Games': 'type_ETC'
    },
    {
        '---Manga---': ['type_MNG', 'type_SCM', 'type_magazine', 'type_comic', 'type_oneshot'],
        'Manga': 'type_MNG',
        'Gekiga': 'type_SCM',
        'Magazine/Anthology': 'type_magazine',
        'Book': 'type_comic',
        'Oneshot/Standalone': 'type_oneshot'
    },
    {
        '---CG + Illustrations---': ['type_ICG'],
        'CG + Illustrations': 'type_ICG'
    },
    {
        '---Novel---': ['type_NRE', 'type_short'],
        'Novel': 'type_NRE',
        'Short Stories': 'type_short'
    },
    {
        '---Video---': ['type_MOV'],
        'Video': 'type_MOV'
    },
    {
        '---Voice / ASMR---': ['type_SOU'],
        'Voice / ASMR': 'type_SOU'
    },
    {
        '---Music---': ['type_MUS'],
        'Music': 'type_MUS'
    },
    {
        '---Tools / Accessories---': ['type_TOL', 'type_IMT', 'type_AMT'],
        'Tools / Accessories': 'type_TOL',
        'Illustration Materials': 'type_IMT',
        'Music Materials': 'type_AMT'
    },
    {
        '---Miscellaneous---': ['type_ET3', 'type_VCM'],
        'Miscellaneous': 'type_ET3',
        'Voiced Comics': 'type_VCM'
    }
  ];

  // Get 'sort by' element
  var sortbox = document.getElementsByClassName('sort_box border_b pb10')[0];

  // Check if element exits
  if (sortbox) {
    // Get all works
    let works = document.getElementsByClassName('work_category');

    // Create new sortbox
    let newSortbox = document.createElement('div');
    newSortbox.className = 'sort_box border_b pb10';

    // Put new sortbox down of 'sort by' element
    document.getElementById('main_inner').insertBefore(newSortbox, sortbox.nextSibling);

    // Create select
    let select = document.createElement('select');
    let divSelect = document.createElement('div');
    let divPadding = document.createElement('div');
    divSelect.className = 'status_select';
    divSelect.textContent = 'Filter by: ';
    divPadding.className = 'display_num_select';

    // Append select
    divSelect.appendChild(select);
    newSortbox.appendChild(divSelect);
    newSortbox.appendChild(divPadding);

    // Get categories present
    categories = filterCategories(categories, works);

    // Populate select
    populateSelect(select, categories);

    // Add event listener for change event
    select.addEventListener("change", selectChanged);
  } else {
    console.log("Error. Couldn't find sortbox element");
    return;
  }

  // Function will leave only categories that are present in current page
  // Recive categories array and works elements array
  function filterCategories(categs, works){
    let categoriesPresent = '';
    let newCategories = [];

    // Get all current works categories
    Array.from(works).forEach(work => {
        let catName = work.className;
        catName = catName.substring(14);

        if(!categoriesPresent.includes(catName)) categoriesPresent += ';' + catName;
    });

    categs.forEach(cat => {
        let categoryDictionary = {};

        // Loop subcategory, add present categories in new dictionary
        for (const key in cat){
            if(Array.isArray(cat[key])) {            // If it's the name of the subcategory add it
                categoryDictionary[key] = cat[key];
            }else{
                if(categoriesPresent.includes(cat[key])) {  // Category found, add it
                    categoryDictionary[key] = cat[key];
                }
            }
        }

        // Only add the subcategory dictionary if there are categories apart from the separator
        if(ObjectSize(categoryDictionary) >= 2) newCategories.push(categoryDictionary);
    });

    return newCategories;
  }

  // Function that triggers on select changed, will show or hide the work/releases depending on category selected
  function selectChanged(event){
      let filter = event.target.value;
      let searchResults = document.getElementById('search_result_img_box');
      let works = searchResults.getElementsByClassName('work_category');
      
      if(filter === 'none'){
        // If filter is none, show all works
        Array.from(works).forEach(work => {
            work.parentNode.parentNode.parentNode.style.display = 'block';
        });
      } else if(filter.includes(';')){
        // Check if current category is a multi one (title of subcategory selected)
        filter = filter.split(';');
        Array.from(works).forEach(work => {
            let found = false;

            // Check each work for each one of the multi category
            filter.forEach(item => {
                if(work.className.includes(item)){
                    work.parentNode.parentNode.parentNode.style.display = 'block';
                    found = true;
                }
            });
            
            // If work wasn't part of the multi category hide it
            if(!found) work.parentNode.parentNode.parentNode.style.display = 'none';
        });
      }else{
        // Simple category, check each work and hide/show it
        Array.from(works).forEach(work => {
            if(work.className.includes(filter)) work.parentNode.parentNode.parentNode.style.display = 'block';
            else work.parentNode.parentNode.parentNode.style.display = 'none';
        });
      }
  }

  function addOptionToSelect(sel, optText, value){
    let option = document.createElement('option');
    option.text = optText;
    option.value = value;
    sel.add(option);
  }

  function populateSelect(sel, categories){
    addOptionToSelect(sel, '---None---', 'none');
    categories.forEach(subcat => {
        for (const key in subcat) {
            if(Array.isArray(subcat[key])){
                addOptionToSelect(sel, key, subcat[key].join(';'));
            }else{
                addOptionToSelect(sel, key, subcat[key]);
            }
        }
    })
  }

  function ObjectSize(obj) {
    var size = 0;
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) size++;
    }
    return size;
  };  
})();