diff --git a/config/install/pwa.config.yml b/config/install/pwa.config.yml new file mode 100644 index 0000000..0016e32 --- /dev/null +++ b/config/install/pwa.config.yml @@ -0,0 +1,5 @@ +site_name: '' +short_name: '' +description: '' +lang: 'en' +urls_to_cache: "/\n\n/offline" diff --git a/config/schema/pwa.schema.yml b/config/schema/pwa.schema.yml new file mode 100644 index 0000000..d0eb6e4 --- /dev/null +++ b/config/schema/pwa.schema.yml @@ -0,0 +1,16 @@ +pwa.config: + type: config_object + label: 'PWA config' + mapping: + site_name: + type: text + label: 'Web app name' + short_name: + type: text + label: 'Short name' + description: + type: text + label: 'Description' + urls_to_cache: + type: text + label: 'URLs to cache' diff --git a/pwa.config_translation.yml b/pwa.config_translation.yml new file mode 100644 index 0000000..b0cb9f1 --- /dev/null +++ b/pwa.config_translation.yml @@ -0,0 +1,5 @@ +pwa.config: + title: 'PWA Translatable config' + base_route_name: pwa.config + names: + - pwa.config diff --git a/pwa.links.menu.yml b/pwa.links.menu.yml new file mode 100644 index 0000000..21fe1f7 --- /dev/null +++ b/pwa.links.menu.yml @@ -0,0 +1,5 @@ +pwa.config: + title: PWA settings + description: 'Progressive web app configuration' + parent: system.admin_config_system + route_name: pwa.config diff --git a/pwa.module b/pwa.module index c770208..0bd5f0e 100644 --- a/pwa.module +++ b/pwa.module @@ -11,9 +11,20 @@ function pwa_page_attachments(array &$attachments) { return; } $attachments['#attached']['library'][] = 'pwa/serviceworker'; + + // Get urls_to_cache from config and split them into an array. $attachments['#attached']['drupalSettings']['pwa'] = [ - 'precache' => ['/', '/offline'], + 'precache' => explode(PHP_EOL, \Drupal::config('pwa.config')->get('urls_to_cache')), ]; + + $manifest_link = [ + '#tag' => 'link', + '#attributes' => [ + 'rel' => 'manifest', + 'href' => '/manifest.json', + ], + ]; + $attachments['#attached']['html_head'][] = [$manifest_link, 'manifest']; } /** @@ -25,4 +36,4 @@ function pwa_theme() { 'variables' => [], ], ]; -} \ No newline at end of file +} diff --git a/pwa.routing.yml b/pwa.routing.yml index b75c419..63b31c6 100644 --- a/pwa.routing.yml +++ b/pwa.routing.yml @@ -1,3 +1,9 @@ +pwa.manifest: + path: /manifest.json + defaults: + _controller: '\Drupal\pwa\Controller\PWAController::pwa_manifest' + requirements: + _permission: 'access pwa' pwa.serviceworker_file_data: path: /serviceworker-pwa defaults: @@ -11,3 +17,10 @@ pwa.offline_page: _controller: '\Drupal\pwa\Controller\PWAController::pwa_offline_page' requirements: _permission: 'access content' +pwa.config: + path: '/admin/config/system/pwa' + defaults: + _form: '\Drupal\pwa\Form\ConfigurationForm' + _title: 'Progressive Web Application' + requirements: + _permission: 'administer site configuration' diff --git a/pwa.services.yml b/pwa.services.yml new file mode 100644 index 0000000..38a0492 --- /dev/null +++ b/pwa.services.yml @@ -0,0 +1,4 @@ +services: + pwa.manifest: + class: Drupal\pwa\manifest + arguments: ['@config.factory', '@language_manager'] diff --git a/src/Controller/PWAController.php b/src/Controller/PWAController.php index 16a9428..76a0b1b 100644 --- a/src/Controller/PWAController.php +++ b/src/Controller/PWAController.php @@ -2,16 +2,68 @@ namespace Drupal\pwa\Controller; -use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\State\StateInterface; +use Drupal\pwa\ManifestInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Response; /** * Default controller for the pwa module. */ -class PWAController extends ControllerBase { +class PWAController implements ContainerInjectionInterface { + /** + * The manifest service. + * + * @var \Drupal\pwa\ManifestInterface + */ + private $manifest; + + /** + * The state. + * + * @var \Drupal\Core\State\StateInterface + */ + private $state; + + /** + * Constructor. + * + * @param \Drupal\pwa\ManifestInterface $manifest + * The manifest service. + * @param \Drupal\Core\State\StateInterface $state + * The system state. + */ + public function __construct(ManifestInterface $manifest, StateInterface $state) { + $this->manifest = $manifest; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('pwa.manifest'), + $container->get('state') + ); + } + + /** + * Fetch the manifest content. + */ + public function pwa_manifest() { + return new Response($this->manifest->getOutput(), 200, [ + 'Content-Type' => 'application/json', + ]); + } + + /** + * Fetch the service worker script content. + */ public function pwa_serviceworker_file_data() { - $query_string = \Drupal::state()->get('system.css_js_query_string') ?: 0; + $query_string = $this->state->get('system.css_js_query_string') ?: 0; $path = drupal_get_path('module', 'pwa'); $data = 'importScripts("/' . $path . '/js/serviceworker.js?' . $query_string . '");'; @@ -21,6 +73,9 @@ class PWAController extends ControllerBase { ]); } + /** + * Fetch the offline page. + */ public function pwa_offline_page() { return [ '#theme' => 'offline', diff --git a/src/Form/ConfigurationForm.php b/src/Form/ConfigurationForm.php new file mode 100644 index 0000000..23f5d5a --- /dev/null +++ b/src/Form/ConfigurationForm.php @@ -0,0 +1,352 @@ +manifest = $manifest; + + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('pwa.manifest') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'pwa_configuration_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('pwa.config'); + + $form['manifest'] = [ + '#type' => 'details', + '#title' => $this->t('Manifest'), + '#open' => TRUE, + ]; + + $form['manifest']['name'] = [ + "#type" => 'textfield', + '#title' => $this->t('Web app name'), + '#description' => $this->t("The name for the application that needs to be displayed to the user."), + '#default_value' => $config->get('site_name'), + '#required' => TRUE, + "#maxlength" => 55, + '#size' => 60, + ]; + + $form['manifest']['short_name'] = [ + "#type" => 'textfield', + "#title" => $this->t('Short name'), + "#description" => $this->t("A short application name, this one gets displayed on the user's homescreen."), + '#default_value' => $config->get('short_name'), + '#required' => TRUE, + '#maxlength' => 25, + '#size' => 30, + ]; + + $form['manifest']['description'] = [ + "#type" => 'textfield', + "#title" => $this->t('Description'), + "#description" => $this->t('The description of your pwa'), + '#default_value' => $config->get('description'), + '#maxlength' => 255, + '#size' => 60, + ]; + + $form['manifest']['theme_color'] = [ + "#type" => 'color', + "#title" => $this->t('Theme color'), + "#description" => $this->t('This color sometimes affects how the application is displayed by the OS.'), + '#default_value' => $config->get('theme_color'), + '#required' => TRUE, + ]; + + $form['manifest']['background_color'] = [ + "#type" => 'color', + "#title" => $this->t('Background color'), + "#description" => $this->t('This color gets shown as the background when the application is launched'), + '#default_value' => $config->get('background_color'), + '#required' => TRUE, + ]; + + $id = $this->getDisplayValue($config->get('display'), TRUE); + + + $form['manifest']['display'] = [ + "#type" => 'select', + "#title" => $this->t('Display type'), + "#description" => $this->t('This determines which UI elements from the OS are displayed.'), + "#options" => [ + '1' => $this->t('fullscreen'), + '2' => $this->t('standalone'), + '3' => $this->t('minimal-ui'), + '4' => $this->t('browser'), + ], + '#default_value' => $id, + '#required' => TRUE, + ]; + + $validators = [ + 'file_validate_extensions' => ['png'], + 'file_validate_image_resolution' => ['512x512', '512x512'], + ]; + + + $form['manifest']['default_image'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use the theme image'), + "#description" => $this->t('This depends on the logo that the theme generates'), + "#default_value" => $config->get('default_image'), + ]; + + $form['manifest']['images'] = [ + '#type' => 'fieldset', + '#states' => [ + 'invisible' => [ + ':input[name="default_image"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['manifest']['images']['image'] = [ + '#type' => 'managed_file', + '#name' => 'image', + '#title' => $this->t('Image'), + '#size' => 20, + '#description' => $this->t('This image is your application icon. (png files only, format: (512x512)'), + '#upload_validators' => $validators, + '#upload_location' => 'public://pwa/', + ]; + + $bobTheHTMLBuilder = '
'; + if ($config->get('default_image') == 0) { + $form['manifest']['images']['current_image'] = [ + '#markup' => $bobTheHTMLBuilder, + '#name' => 'current image', + '#id' => 'current_image', + ]; + } + + $form['service_worker'] = [ + '#type' => 'details', + '#title' => $this->t('Service worker'), + '#open' => TRUE, + ]; + + $form['service_worker']['urls_to_cache'] = [ + '#type' => 'textarea', + '#title' => $this->t('URLs to cache on install'), + '#description' => $this->t('Cache these URLs when the Service Worker is installed. If a URL is a page all its CSS and JS will be cached automatically.'), + '#default_value' => $config->get('urls_to_cache'), + '#rows' => 15 + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * + * function converts an id to a display string or a string to an id + * + * @param $value + * @param boolean $needId + * + * @return int|string + */ + private function getDisplayValue($value, $needId) { + if ($needId) { + $id = 1; + switch ($value) { + case 'standalone': + $id = 2; + break; + case 'minimal-ui': + $id = 3; + break; + case 'browser': + $id = 4; + break; + } + return $id; + } + else { + $display = ''; + switch ($value) { + case 1: + $display = 'fullscreen'; + break; + case 2: + $display = 'standalone'; + break; + case 3: + $display = 'minimal-ui'; + break; + case 4: + $display = 'browser'; + break; + } + } + return $display; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $default_image = $form_state->getValue('default_image'); + $img = $form_state->getValue(['image', 0]); + $config = $this->config('pwa.config'); + + if ($config->get('default_image') && !$default_image && !isset($img)) { + $form_state->setErrorByName('image', $this->t('Upload a image, or chose the theme image.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('pwa.config'); + + $display = $this->getDisplayValue($form_state->getValue('display'), FALSE); + + $fid = $form_state->getValue(['image', 0]); + $default_image = $form_state->getValue('default_image'); + + if ($config->get('default_image') == 0) { + if (isset($fid) || $default_image == 1) { + $this->manifest->deleteImage(); + } + } + + $configTheme = $this->config('system.theme'); + $nameOfDefaultTheme = $configTheme->get('default'); + + if ($default_image) { + $theme_image = theme_get_setting('logo', $nameOfDefaultTheme)['url']; + if (substr($theme_image, strlen($theme_image) - 3, 3) != 'png') { + $this->messenger() + ->addWarning($this->t('The theme image is not a .png file, your users may not be able to add this website to the homescreen.')); + } + $image_size = getimagesize($theme_image); + if ($image_size[0] == $image_size[1]) { + $this->messenger() + ->addWarning($this->t('The theme image is not a square, your application image maybe altered (recommended size: 512x512).')); + } + } + + + $config + ->set('site_name', $form_state->getValue('name')) + ->set('short_name', $form_state->getValue('short_name')) + ->set('theme_color', $form_state->getValue('theme_color')) + ->set('background_color', $form_state->getValue('background_color')) + ->set('description', $form_state->getValue('description')) + ->set('display', $display) + ->set('default_image', $default_image) + ->set('urls_to_cache', $form_state->getValue('urls_to_cache')) + ->save(); + + if (!empty($fid)) { + + $file = File::load($fid); + + $file_usage = \Drupal::service('file.usage'); + $file->setPermanent(); + $file->save(); + + $file_usage->add($file, 'PWA', 'PWA', $this->currentUser()->id()); + + //save new image + $files_path = file_create_url("public://pwa") . '/'; + + if (substr($files_path, 0, 7) == 'http://') { + $files_path = str_replace('http://', '', $files_path); + } + elseif (substr($files_path, 0, 8) == 'https://') { + $files_path = str_replace('https://', '', $files_path); + } + if (substr($files_path, 0, 4) == 'www.') { + $files_path = str_replace('www.', '', $files_path); + } + $host = $this->getRequest()->server->get('HTTP_HOST'); + if (substr($files_path, 0, strlen($host)) == $host) { + $files_path = str_replace($host, '', $files_path); + } + + + $file_uri = $files_path . $file->getFilename(); + $file_path = \Drupal::service('file_system') + ->realpath(file_default_scheme() . "://") . '/pwa/' . $file->getFilename(); + + $config->set('image', $file_uri)->save(); + + $newSize = 192; + $oldSize = 512; + + $src = imagecreatefrompng($file_path); + $dst = imagecreatetruecolor($newSize, $newSize); + + //make transparent background + $color = imagecolorallocatealpha($dst, 0, 0, 0, 127); + imagefill($dst, 0, 0, $color); + imagesavealpha($dst, TRUE); + + imagecopyresampled($dst, $src, 0, 0, 0, 0, $newSize, $newSize, $oldSize, $oldSize); + $path_to_copy = \Drupal::service('file_system') + ->realpath(file_default_scheme() . "://") . '/pwa/' . $file->getFilename() . 'copy.png'; + if ($stream = fopen($path_to_copy, 'w+')) { + imagepng($dst, $stream); + $config->set('image_small', $files_path . $file->getFilename() . 'copy.png') + ->save(); + } + } + + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['pwa.config']; + } +} diff --git a/src/Manifest.php b/src/Manifest.php new file mode 100644 index 0000000..eee8626 --- /dev/null +++ b/src/Manifest.php @@ -0,0 +1,146 @@ +configFactory = $config_factory; + $this->languageManager = $language_manager; + + $this->manifestUri = '/manifest.json'; + } + + /** + * {@inheritdoc} + */ + public function getOutput() { + // Get values. + $values = $this->getCleanValues(); + + $manifest_data = [ + 'name' => $values['site_name'], + 'short_name' => $values['short_name'], + 'display' => $values['display'], + 'background_color' => $values['background_color'], + 'theme_color' => $values['theme_color'], + 'description' => $values['description'], + 'lang' => $values['lang'], + 'icons' => [ + [ + "src" => $values['image_small'], + "sizes" => '192x192', + "type" => 'image/png', + ], + [ + "src" => $values['image'], + "sizes" => '512x512', + "type" => 'image/png', + ], + ], + 'start_url' => '/', + 'scope' => '/', + ]; + + return json_encode($manifest_data); + } + + /** + * {@inheritdoc} + */ + public function deleteImage() { + $config = $this->configFactory->get('pwa.config'); + $path = getcwd() . $config->get('image'); + unlink($path); + $path .= 'copy.png'; + unlink($path); + } + + /** + * Checks the values in config and add default value if necessary. + * + * @return array + * Values from the configuration. + */ + private function getCleanValues() { + $output = []; + + // Change configuration language. + $lang = $this->languageManager->getCurrentLanguage()->getId(); + $language = $this->languageManager->getLanguage($lang); + $this->languageManager->setConfigOverrideLanguage($language); + $config_get = $this->configFactory->get('pwa.config'); + + $config = $this->configFactory->getEditable('pwa.config'); + + $input = $config->get(); + + if ($input['default_image'] == TRUE) { + $input['image'] = theme_get_setting('logo')['url']; + $input['image_small'] = $input['image']; + } + + foreach ($input as $key => $value) { + if ($value !== '') { + $output[$key] = $value; + } + elseif ($config->get($key) !== '') { + $output[$key] = $config->get($key); + } + else { + if ($key === 'background_color' || $key === 'theme_color') { + $output[$key] = '#ffffff'; + $config->set($key, '#ffffff')->save(); + } + else { + if ($key === 'image' && $input['dafault_image'] != 1) { + $output[$key] = 'url/to/default/img'; + $config->set($key, 'url/to/default/img')->save(); + } + else { + if ($key == 'display') { + $output[$key] = 'standalone'; + $config->set($key, 'standalone')->save(); + } + else { + $output[$key] = 'default value for ' . $key . ', go to configuration to change'; + $config->set($key, 'default value for ' . $key . ', go to configuration to change') + ->save(); + } + } + } + } + } + + // Values that's not required. + $output['description'] = $config_get->get('description'); + + return $output; + } + +} diff --git a/src/ManifestInterface.php b/src/ManifestInterface.php new file mode 100644 index 0000000..94b9c6d --- /dev/null +++ b/src/ManifestInterface.php @@ -0,0 +1,23 @@ +