Source

frontend/controllers/module_update_controller.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 Mousetrap = require('mousetrap');
const eventController = require('../controllers/event_controller.js');
const { html } = require('../helpers/ezra_helper.js');
const swordModuleHelper = require('../helpers/sword_module_helper.js');

/**
 * This controller manages the update of SWORD modules. 
 * @module moduleUpdateController
 * @category Controller
 */

var repoUpdateInProgress = false;
var moduleUpdateInitiated = false;
var moduleUpdateCompleted = false;

module.exports.init = function() {
  eventController.subscribe('on-repo-update-started', () => {
    disableDialogButtons();
    repoUpdateInProgress = true;
    clearUpdatedModuleList();
  });

  eventController.subscribe('on-repo-update-completed', () => {
    refreshUpdatedModuleList();
    repoUpdateInProgress = false;
  });
};

module.exports.showModuleUpdateDialog = async function() {
  const dialogBoxTemplate = html`

  <link href="css/module_settings_assistant.css" media="screen" rel="stylesheet" type="text/css" />

  <div id="module-update-dialog" class="module-settings-assistant">
    <div id="module-update-dialog-content" class="container" style="padding-top: 0.5em">

      <div id="module-update-step-1" class="module-settings-assistant-init">
        <section>
          <p i18n="[html]module-assistant.intro-text"
             data-i18n-options='{ "module_type" : "$t(module-assistant.module-type-generic)" }'></p>
          <p class="repository-explanation assistant-note"
             i18n="[html]module-assistant.step-repositories.what-is-repository"
             data-i18n-options='{ "module_type" : "$t(module-assistant.module-type-generic)" }'></p>
        </section>

        <section class="module-settings-assistant-internet-usage">
          <p i18n="[html]module-assistant.internet-usage-note"></p>
        </section>

        <div class="module-assistant-type-buttons">
          <button id="update-modules-button"
                  class="fg-button ui-corner-all ui-state-default"
                  i18n="module-assistant.update-modules"
                  data-i18n-options='{ "module_type" : "$t(module-assistant.module-type-generic)" }'></button>
        </div>
      </div>
    </div>
  </div>
  `;

  const dialogBoxTemplateStep2 = html`
    <div id="module-update-step-2">
      <update-repositories></update-repositories>

      <p id="module-update-header" style="display: none; margin-top: 1em; float: left;" i18n="general.module-updates-available"></p>
      <p id="module-update-header-up-to-date" style="display: none; margin-top: 4em; float: left; text-align: center; width: 100%;" i18n="general.modules-up-to-date"></p>
      <loading-indicator id="module-update-loading-indicator" style="display: none; float: right;"></loading-indicator>

      <table id="module-update-list" style="clear: both; display: none;">
        <thead>
          <tr>
            <th i18n="general.module-name" style="text-align: left; min-width: 13em"></th>
            <th i18n="general.module-current-version" style="text-align: left; width: 8em;"></th>
            <th i18n="general.module-new-version" style="text-align: left; width: 8em;"></th>
            <th style="width: 5em;"></th>
          </tr>
        </thead>
        <tbody id="module-update-list-tbody">
        </tbody>
      </table>
    </div>
  `;

  moduleUpdateInitiated = false;

  return new Promise((resolve) => {

    document.querySelector('#boxes').appendChild(dialogBoxTemplate.content);
    const $dialogBox = $('#module-update-dialog');
    $dialogBox.localize();

    uiHelper.configureButtonStyles('#module-update-step-1');

    const appContainerWidth = $(window).width() - 10;
    var dialogWidth = null;

    if (appContainerWidth < 1100) {
      dialogWidth = appContainerWidth;
    } else {
      dialogWidth = 1100;
    }

    var dialogHeight = $(window).height() * 0.8;

    var confirmed = false;
    const offsetLeft = ($(window).width() - dialogWidth)/2;
    let fullscreen = platformHelper.isCordova();

    let dialogOptions = uiHelper.getDialogOptions(dialogWidth, dialogHeight, false, [offsetLeft, 80], false, fullscreen);
    dialogOptions.dialogClass = 'ezra-dialog module-update-dialog';
    dialogOptions.title = i18n.t('general.update-modules');
    dialogOptions.draggable = true;
    dialogOptions.modal = true;
    dialogOptions.buttons = {};

    dialogOptions.close = () => {
      $dialogBox.dialog('destroy');
      $dialogBox.remove();
      resolve(confirmed);
    };

    Mousetrap.bind('esc', () => { $dialogBox.dialog("close"); });

    $dialogBox.dialog(dialogOptions);
    uiHelper.fixDialogCloseIconOnAndroid('module-update-dialog');

    document.getElementById('update-modules-button').addEventListener('click', () => {
      let dialogContent = document.getElementById('module-update-dialog-content');
      dialogContent.innerHTML = dialogBoxTemplateStep2.innerHTML;
      $dialogBox.localize();

      let buttons = {};

      buttons[i18n.t('general.update')] = function(event) {
        if (event.target.closest('button').classList.contains('ui-state-disabled')) {
          return;
        }

        performModuleUpdate();
        confirmed = true;
      };

      buttons[i18n.t('general.cancel')] = function() {
        if (!moduleUpdateInitiated || moduleUpdateCompleted) {
          $dialogBox.dialog('destroy');
          $dialogBox.remove();
          resolve(confirmed);
        }
      };

      $dialogBox.dialog("option", "buttons", buttons);

      if (!repoUpdateInProgress) {
        refreshUpdatedModuleList();
      }

      disableDialogButtons();
    });
  });
};

function clearUpdatedModuleList() {
  document.getElementById('module-update-header').style.display = 'none';
  document.getElementById('module-update-header-up-to-date').style.display = 'none';
  document.getElementById('module-update-list').style.display = 'none';

  let moduleUpdateList = document.getElementById('module-update-list-tbody');
  moduleUpdateList.innerHTML = '';
}

function refreshUpdatedModuleList() {
  document.getElementById('module-update-loading-indicator').style.display = 'block';

  setTimeout(() => {
    ipcNsi.getUpdatedModules().then((updatedModules) => {
      let moduleUpdateList = document.getElementById('module-update-list-tbody');

      if (updatedModules.length == 0) {

        document.getElementById('module-update-header').style.display = 'none';
        document.getElementById('module-update-header-up-to-date').style.display = 'block';

      } else {
        document.getElementById('module-update-header-up-to-date').style.display = 'none';

        updatedModules.forEach(async (module) => {
          let moduleRow = document.createElement('tr');
          moduleRow.setAttribute('module-code', module.name);

          let nameCell = document.createElement('td');
          nameCell.style.paddingRight = '1em';
          nameCell.innerText = module.description;

          let oldVersionCell = document.createElement('td');
          let localModule = await ipcNsi.getLocalModule(module.name);
          oldVersionCell.innerText = localModule.version;
          
          let newVersionCell = document.createElement('td');
          newVersionCell.innerText = module.version;

          let statusCell = document.createElement('td');
          statusCell.style.textAlign = 'center';
          statusCell.classList.add('status');

          let loadingIndicator = document.createElement('loading-indicator');
          loadingIndicator.style.display = 'none';
          statusCell.appendChild(loadingIndicator);

          moduleRow.appendChild(nameCell);
          moduleRow.appendChild(oldVersionCell);
          moduleRow.appendChild(newVersionCell);
          moduleRow.appendChild(statusCell);
          moduleUpdateList.appendChild(moduleRow);
        });

        document.getElementById('module-update-header').style.display = 'block';
        document.getElementById('module-update-list').style.display = 'block';
      }

      document.getElementById('module-update-loading-indicator').style.display = 'none';

      enableDialogButtons();

      if (updatedModules.length == 0) {
        disableUpdateButton();
      }
    });
  }, 100);
}

function disableDialogButtons() {
  let moduleUpdateDialog = document.querySelector('.module-update-dialog');

  if (moduleUpdateDialog != null) {
    let buttonSet = moduleUpdateDialog.querySelector('.ui-dialog-buttonset');

    if (buttonSet != null) {
      let dialogButtons = buttonSet.querySelectorAll('button');
      let updateButton = dialogButtons[0];
      let cancelButton = dialogButtons[1];
      updateButton.classList.add('ui-state-disabled');
      cancelButton.classList.add('ui-state-disabled');
    }

    let dialogCloseButton = moduleUpdateDialog.querySelector('.ui-dialog-titlebar-close');
    let updateRepoDataButton = moduleUpdateDialog.querySelector('.update-repo-data');

    if (updateRepoDataButton != null) {
      updateRepoDataButton.classList.add('ui-state-disabled');
    }

    if (dialogCloseButton != null) {
      dialogCloseButton.style.display = 'none';
    }
  }
}

function enableDialogButtons() {
  let moduleUpdateDialog = document.querySelector('.module-update-dialog');

  if (moduleUpdateDialog != null) {
    let dialogButtons = moduleUpdateDialog.querySelector('.ui-dialog-buttonset').querySelectorAll('button');
    let updateButton = dialogButtons[0];
    let cancelButton = dialogButtons[1];
    let dialogCloseButton = moduleUpdateDialog.querySelector('.ui-dialog-titlebar-close');
    let updateRepoDataButton = moduleUpdateDialog.querySelector('.update-repo-data');

    updateButton.classList.remove('ui-state-disabled');
    cancelButton.classList.remove('ui-state-disabled');
    updateRepoDataButton.classList.remove('ui-state-disabled');
    dialogCloseButton.style.removeProperty('display');
  }
}

function enableFinishButton() {
  let moduleUpdateDialog = document.querySelector('.module-update-dialog');

  if (moduleUpdateDialog != null) {
    let dialogButtons = moduleUpdateDialog.querySelector('.ui-dialog-buttonset').querySelectorAll('button');
    let cancelButton = dialogButtons[1];

    cancelButton.firstChild.innerText = i18n.t('general.finish');
    cancelButton.classList.remove('ui-state-disabled');
  }
}

function enableDialogCloseButton() {
  let moduleUpdateDialog = document.querySelector('.module-update-dialog');

  if (moduleUpdateDialog != null) {
    let dialogCloseButton = moduleUpdateDialog.querySelector('.ui-dialog-titlebar-close');
    dialogCloseButton.style.removeProperty('display');
  }
}

function disableUpdateButton() {
  let moduleUpdateDialog = document.querySelector('.module-update-dialog');

  if (moduleUpdateDialog != null) {
    let dialogButtons = moduleUpdateDialog.querySelector('.ui-dialog-buttonset').querySelectorAll('button');
    let updateButton = dialogButtons[0];
    updateButton.classList.add('ui-state-disabled');
  }
}

async function performModuleUpdate() {
  if (moduleUpdateInitiated) {
    return;
  }

  moduleUpdateInitiated = true;
  let moduleUpdateList = document.getElementById('module-update-list-tbody');
  let rows = moduleUpdateList.querySelectorAll('tr');
  let previousLoadingIndicator = null;

  disableDialogButtons();

  for (let i = 0; i < rows.length; i++) {
    let tr = rows[i];

    let moduleCode = tr.getAttribute('module-code');
    let statusCell = tr.querySelector('.status');
    let loadingIndicator = statusCell.firstChild;
    previousLoadingIndicator = loadingIndicator;

    loadingIndicator.style.display = 'block';

    // Installation on top of an existing installation is buggy!
    // Therefore, the explicit uninstall step is quite important!
    await ipcNsi.uninstallModule(moduleCode);

    await ipcNsi.installModule(moduleCode);

    if (previousLoadingIndicator != null) {
      previousLoadingIndicator.style.display = 'none';

      let successIndicator = document.createElement('i');
      successIndicator.style.color = 'var(--checkmark-success-color)';
      successIndicator.classList.add('fas', 'fa-check', 'fa-lg');
      statusCell.appendChild(successIndicator);
    }
  }

  moduleUpdateCompleted = true;

  // The sword module helper may still have a cached version of one of the upgraded modules.
  // Therefore it is important to reset it.
  swordModuleHelper.resetModuleCache();

  enableDialogCloseButton();
  enableFinishButton();
}