Source

frontend/components/module_assistant/step_modules.js

/* This file is part of Ezra Bible App.

   Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>

   Ezra Bible App is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 2 of the License, or
   (at your option) any later version.

   Ezra Bible App is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Ezra Bible App. See the file LICENSE.
   If not, see <http://www.gnu.org/licenses/>. */


const { html } = require('../../helpers/ezra_helper.js');
const assistantController = require('./assistant_controller.js');
const i18nHelper = require('../../helpers/i18n_helper.js');
const assistantHelper = require('./assistant_helper.js');
require('../generic/loading_indicator.js');


const ICON_INFO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>`;

var unlockInfo = {};

const template = html`
<style>
  #module-step-wrapper {
    display: flex;
    height: 100%;
    border-radius: 5px;
    overflow: hidden;
  }
  #module-list#module-list {
    width: 100%;
    padding-right: 1em;
  }
  .feature-filter-wrapper {
    margin-bottom: 1em;
    display: none;
  }
  #module-list > p:first-child {
    margin-top: 0;
  }
  #module-list-intro {
    clear: both; 
    margin-bottom: 2em;
  }

  #module-info {
    overflow-y: auto;
    display: block;
    padding: 1em;
    position: relative;
    z-index: 10;
    color: #696969;
    width: 100%;
  }
  .darkmode--activated #module-info, .darkmode--activated #module-info p {
    color: #929292;
  }
  #module-info .background {
    position: absolute;
    z-index: -1;
    width: 17em;
    margin-top: -4em;
    margin-left: -4em;
    opacity: 0.1;
    fill: #696969;
  }
</style>

<div id="module-step-wrapper">
  <div id="module-list" class="scrollable">
    <p id="module-list-intro" class="intro" i18n="module-assistant.step-modules.select-module"></p>
    
    <p><b id="module-display-preferences-header" i18n="module-assistant.step-modules.module-display-preferences"></b></p>

    <div id="bible-module-feature-filter" class="feature-filter-wrapper">
      <label>
        <input id="headings-feature-filter" class="module-feature-filter" type="checkbox"/> 
        <span id="headings-feature-filter-label" for="headings-feature-filter" i18n="$t(module-assistant.step-modules.module-with, {'feature_type': 'general.module-headings'})"></span>
      </label>
      <label>
        <input id="strongs-feature-filter" class="module-feature-filter" type="checkbox"/>
        <span id="strongs-feature-filter-label" for="strongs-feature-filter" i18n="$t(module-assistant.step-modules.module-with, {'feature_type': 'general.module-strongs'})"></span>
      </label>
    </div>

    <div id="dict-module-feature-filter" class="feature-filter-wrapper">
      <label>
        <input id="hebrew-strongs-dict-feature-filter" class="module-feature-filter" type="checkbox"/>
        <span id="hebrew-strongs-dict-feature-filter-label" for="hebrew-strongs-dict-feature-filter" i18n="$t(module-assistant.step-modules.module-with, {'feature_type': 'general.module-hebrew-strongs-dict'})"></span>
      </label>
      <label>
        <input id="greek-strongs-dict-feature-filter" class="module-feature-filter" type="checkbox"/>
        <span id="greek-strongs-dict-feature-filter-label" for="greek-strongs-dict-feature-filter" i18n="$t(module-assistant.step-modules.module-with, {'feature_type': 'general.module-greek-strongs-dict'})"></span>
      </label>
    </div>


    <div id="filtered-module-list"></div>
  </div>  

  <div id="module-info" class="scrollable">
    <div class="background">${ICON_INFO}</div>
    <div id="module-info-content" i18n="module-assistant.step-modules.show-detailed-module-info"></div>
    <loading-indicator style="display: none"></loading-indicator>
  </div>
</div>
`;

/**
 * component displays available for installation modules from selected repositories and languages
 * @module StepModules
 * @example
 * <step-modules></step-modules>
 * @category Component
 */
class StepModules extends HTMLElement {

  constructor() {
    super();

    this.unlockDialog = null;
  }

  async connectedCallback() {
    this.appendChild(template.content.cloneNode(true));
    assistantHelper.localizeContainer(this, assistantController.get('moduleType'));
    
    this.querySelectorAll('.module-feature-filter').forEach(checkbox => checkbox.addEventListener('click', async () => {
      this._listFilteredModules();
    }));
    
    const filteredModuleList = this.querySelector('#filtered-module-list');
    filteredModuleList.addEventListener('itemChanged', (e) => this._handleCheckboxClick(e));
  }
  
  async listModules() {
    const moduleType = assistantController.get('moduleType');
    if (moduleType == 'BIBLE') {
      this.querySelector("#bible-module-feature-filter").style.display = 'block';
    } else if (moduleType == 'DICT') {
      this.querySelector("#dict-module-feature-filter").style.display = 'block';
    } else {
      this.querySelector("#module-display-preferences-header").style.display = 'none';
    }

    await this._listFilteredModules();
  }

  async _listFilteredModules() {
    const filteredModuleList = this.querySelector('#filtered-module-list');
    filteredModuleList.innerHTML = '';

    const headingsFilter = this.querySelector('#headings-feature-filter').checked;
    const strongsFilter = this.querySelector('#strongs-feature-filter').checked;

    const hebrewStrongsFilter = this.querySelector('#hebrew-strongs-dict-feature-filter').checked;
    const greekStrongsFilter = this.querySelector('#greek-strongs-dict-feature-filter').checked;

    const languageCodes = [...assistantController.get('selectedLanguages')].sort((codeA, codeB) => {
      return assistantHelper.sortByText(i18nHelper.getLanguageName(codeA), i18nHelper.getLanguageName(codeB));
    });

    let renderHeader = false;
    if (languageCodes.length > 1) {
      renderHeader = true;
    }

    const repositories = [...assistantController.get('selectedRepositories')];

    const installedModules = new Set(assistantController.get('installedModules'));

    const sectionOptions = {columns: 1, 
                            rowGap: '1.5em', 
                            info: true};

    for (const language of languageCodes) {
      const modules = await getModulesByLang(language, 
                                             repositories, 
                                             installedModules, 
                                             headingsFilter, 
                                             strongsFilter, 
                                             hebrewStrongsFilter, 
                                             greekStrongsFilter);
      
      if (modules.size > 0) {
        const langModuleSection = assistantHelper.listCheckboxSection(modules,
                                                                      installedModules,
                                                                      renderHeader ? i18nHelper.getLanguageName(language) : undefined,
                                                                      sectionOptions);
        filteredModuleList.append(langModuleSection);
        filteredModuleList.querySelectorAll('.module-info-button').forEach(element => {
          element.addEventListener('click', (e) => this._handleInfoClick(e));
        });
      }  
    }
  }

  _handleCheckboxClick(event) {
    const checkbox = event.target;
    const moduleId = event.detail.code;
    const checked = event.detail.checked;

    if (checked) {
      assistantController.add('selectedModules', moduleId);
    } else {
      assistantController.remove('selectedModules', moduleId);
    }

    if (!checkbox.hasAttribute('locked')) {
      return;
    }

    if (checked) {
      this.unlockDialog.show(moduleId, unlockInfo[moduleId], checkbox);
    } else {
      // Checkbox unchecked!
      // Reset the unlock key for this module
      this.unlockDialog.resetKey(moduleId);
    }
  }

  _handleInfoClick(event) {

    const moduleCode = event.currentTarget.parentElement.firstElementChild.code;

    const moduleInfo = this.querySelector('#module-info');
    if (moduleInfo.getAttribute('code') !== moduleCode) {
      moduleInfo.setAttribute('code', moduleCode);

      const moduleInfoContent = moduleInfo.querySelector('#module-info-content');
      const loadingIndicator = moduleInfo.querySelector('loading-indicator');

      moduleInfoContent.innerHTML = '';
      loadingIndicator.show();

      setTimeout(async () => {
        const swordModuleHelper = require('../../helpers/sword_module_helper.js');
        moduleInfoContent.innerHTML = await swordModuleHelper.getModuleInfo(moduleCode, true);
        loadingIndicator.hide();
      }, 100);
    }  
  }
}

customElements.define('step-modules', StepModules);
module.exports = StepModules;

async function getModulesByLang(languageCode, repositories, installedModules, headingsFilter, strongsFilter, hebrewStrongsFilter, greekStrongsFilter) {
  var currentLangModules = new Map();

  for (const currentRepo of repositories) {
    const currentRepoLangModules = await ipcNsi.getRepoModulesByLang(currentRepo,
                                                                     languageCode,
                                                                     assistantController.get('moduleType'),
                                                                     headingsFilter,
                                                                     strongsFilter,
                                                                     hebrewStrongsFilter,
                                                                     greekStrongsFilter);
    
    const hiddenModules = [
      'GerHfa2002', // The GerHfa2002 (Hoffnung für alle) is excluded from the list, since you cannot purchase an unlock key anymore.
      'Personal'    // The Personal Commentary module is not useful in the context of Ezra Bible App.
    ];

    for (const swordModule of currentRepoLangModules) {

      if (hiddenModules.includes(swordModule.name)) {
        continue;
      }

      // We do not support Image modules
      if (swordModule.category == "Images") {
        continue;
      }

      let moduleInfo = {
        code: swordModule.name,
        text: `${swordModule.description} [${swordModule.name}]`,
        description: `${swordModule.repository}; ${i18n.t('general.module-version')}: ${swordModule.version}; ${i18n.t("general.module-size")}: ${Math.round(swordModule.size / 1024)} KB`,
      };

      if (swordModule.locked) {
        unlockInfo[swordModule.name] = swordModule.unlockInfo;

        moduleInfo['icon'] = 'lock';
        moduleInfo['locked'] = "locked";
        moduleInfo['title'] = assistantHelper.localizeText("module-assistant.unlock.module-lock-info", 
                                                           assistantController.get('moduleType'));
      }
      
      if (installedModules.has(swordModule.name)) {
        moduleInfo['disabled'] = true;
        moduleInfo['title'] = assistantHelper.localizeText("module-assistant.step-modules.module-already-installed", 
                                                           assistantController.get('moduleType'));
      }

      currentLangModules.set(swordModule.description, moduleInfo);
    }
  }

  return currentLangModules;
}