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 @@
+