From bf29e1b62a1013242b109cec7cd23151aa36419d Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Wed, 10 Jan 2024 11:28:03 +0100 Subject: [PATCH] Takes screenshots of your applications and inject them to the manifest file (#20) --- README.md | 2 +- composer.json | 7 +- src/Command/GenerateManifestCommand.php | 56 +++++++-- src/DependencyInjection/Configuration.php | 109 +++++++++++++----- .../SpomkyLabsPwaExtension.php | 5 +- src/Resources/config/services.php | 2 - 6 files changed, 133 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index d7e663e..51b6038 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Please have a look at the [Web app manifests](https://developer.mozilla.org/en-U # Installation -Install the bundle with Composer: `composer require spomky-labs/phpwa`. +Install the bundle with Composer: `composer require --dev spomky-labs/phpwa`. This project follows the [semantic versioning](http://semver.org/) strictly. diff --git a/composer.json b/composer.json index 376e73c..d3f591b 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "symfony/config": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4", + "symfony/finder": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", "symfony/routing": "^6.4|^7.0" @@ -43,7 +43,7 @@ "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^10.0", - "rector/rector": "^0.18", + "rector/rector": "^0.19", "symfony/framework-bundle": "^6.4|^7.0", "symfony/phpunit-bridge": "^6.4|^7.0", "symplify/easy-coding-standard": "^12.0" @@ -57,6 +57,7 @@ }, "suggest": { "ext-gd": "Required to generate icons (or Imagick).", - "ext-imagick": "Required to generate icons (or GD)." + "ext-imagick": "Required to generate icons (or GD).", + "symfony/panther": "For generating screenshots directly from your application" } } diff --git a/src/Command/GenerateManifestCommand.php b/src/Command/GenerateManifestCommand.php index 3d47e00..782e39a 100644 --- a/src/Command/GenerateManifestCommand.php +++ b/src/Command/GenerateManifestCommand.php @@ -4,6 +4,7 @@ namespace SpomkyLabs\PwaBundle\Command; +use Facebook\WebDriver\WebDriverDimension; use JsonException; use RuntimeException; use SpomkyLabs\PwaBundle\ImageProcessor\ImageProcessor; @@ -18,6 +19,7 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Panther\Client; use Symfony\Component\Routing\RouterInterface; use function count; use function dirname; @@ -33,16 +35,24 @@ final class GenerateManifestCommand extends Command { private readonly MimeTypes $mime; + private readonly null|Client $webClient; + public function __construct( private readonly null|ImageProcessor $imageProcessor, + #[Autowire('@pwa.web_client')] + null|Client $webClient, #[Autowire('%spomky_labs_pwa.config%')] - private readonly array $config, + private readonly array $config, #[Autowire('%spomky_labs_pwa.dest%')] - private readonly array $dest, - private readonly Filesystem $filesystem, + private readonly array $dest, + private readonly Filesystem $filesystem, private readonly FileLocator $fileLocator, - private readonly ?RouterInterface $router = null, + private readonly ?RouterInterface $router = null, ) { + if ($webClient === null && class_exists(Client::class)) { + $webClient = Client::createChromeClient(); + } + $this->webClient = $webClient; $this->mime = MimeTypes::getDefault(); parent::__construct(); } @@ -242,13 +252,36 @@ private function processScreenshots(SymfonyStyle $io, array $manifest): array|in $manifest['screenshots'] = []; $config = []; foreach ($this->config['screenshots'] as $screenshot) { - $src = $screenshot['src']; - if (! $this->filesystem->exists($src)) { - continue; + if (isset($screenshot['src'])) { + $src = $screenshot['src']; + if (! $this->filesystem->exists($src)) { + continue; + } + foreach ($this->findImages($src) as $image) { + $data = $screenshot; + $data['src'] = $image; + $config[] = $data; + } } - foreach ($this->findImages($src) as $image) { + if (isset($screenshot['path'])) { + $path = $screenshot['path']; + $height = $screenshot['height']; + $width = $screenshot['width']; + unset($screenshot['path'], $screenshot['height'], $screenshot['width']); + + $client = clone $this->webClient; + $client->request('GET', $path); + $tmpName = $this->filesystem->tempnam('', 'pwa-'); + $client->manage() + ->window() + ->setSize(new WebDriverDimension($width, $height)); + $client->manage() + ->window() + ->fullscreen(); + $client->takeScreenshot($tmpName); $data = $screenshot; - $data['src'] = $image; + $data['src'] = $tmpName; + $data['delete'] = true; $config[] = $data; } } @@ -259,6 +292,8 @@ private function processScreenshots(SymfonyStyle $io, array $manifest): array|in $io->error(sprintf('Unable to read the icon "%s"', $screenshot['src'])); return self::FAILURE; } + $delete = $screenshot['delete'] ?? false; + unset($screenshot['delete']); $screenshotManifest = $this->storeScreenshot( $data, $screenshot['format'] ?? null, @@ -271,6 +306,9 @@ private function processScreenshots(SymfonyStyle $io, array $manifest): array|in $screenshotManifest['platform'] = $screenshot['platform']; } $manifest['screenshots'][] = $screenshotManifest; + if ($delete) { + $this->filesystem->remove($screenshot['src']); + } } return $manifest; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 2a4a6cc..8e162cc 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -77,38 +77,8 @@ private function setupShortcuts(ArrayNodeDefinition $node): void private function setupScreenshots(ArrayNodeDefinition $node): void { $node->children() - ->arrayNode('screenshots') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->info('The screenshots of the application.') - ->arrayPrototype() - ->children() - ->scalarNode('src') - ->isRequired() - ->info('The path to the screenshot.') - ->example('screenshot/lowres.webp') - ->end() - ->scalarNode('form_factor') - ->info('The form factor of the screenshot. Will guess the form factor if not set.') - ->example(['wide', 'narrow']) - ->end() - ->scalarNode('label') - ->info('The label of the screenshot.') - ->example('Homescreen of Awesome App') - ->end() - ->scalarNode('platform') - ->info('The platform of the screenshot.') - ->example( - ['android', 'windows', 'chromeos', 'ipados', 'ios', 'kaios', 'macos', 'windows', 'xbox'] - ) - ->end() - ->scalarNode('format') - ->info('The format of the screenshot. Will convert the file if set.') - ->example(['jpg', 'png', 'webp']) - ->end() - ->end() - ->end() + ->append($this->getScreenshotsNode()) + ->end() ; } @@ -293,6 +263,10 @@ private function setupSimpleOptions(ArrayNodeDefinition $node): void ->info('The image processor to use to generate the icons of different sizes.') ->example(GDImageProcessor::class) ->end() + ->scalarNode('web_client') + ->defaultNull() + ->info('The Panther Client for generating screenshots. If not set, the default client will be used.') + ->end() ->scalarNode('icon_folder') ->defaultValue('%kernel.project_dir%/public/pwa') ->info('The folder where the icons will be generated.') @@ -468,4 +442,75 @@ private function setupServiceWorker(ArrayNodeDefinition $node): void ->end() ->end(); } + + private function getScreenshotsNode(): ArrayNodeDefinition + { + $treeBuilder = new TreeBuilder('screenshots'); + $node = $treeBuilder->getRootNode(); + assert($node instanceof ArrayNodeDefinition); + $node + ->treatFalseLike([]) + ->treatTrueLike([]) + ->treatNullLike([]) + ->arrayPrototype() + ->validate() + ->ifTrue(static fn (array $v): bool => ! (isset($v['src']) xor isset($v['path']))) + ->thenInvalid('Either "src", "route" or "path" must be set.') + ->end() + ->validate() + ->ifTrue(static function (array $v): bool { + if (isset($v['src'])) { + return false; + } + + if (! isset($v['height']) || ! isset($v['width'])) { + return true; + } + + return false; + }) + ->thenInvalid('When using "path", "height" and "width" must be set.') + ->end() + ->children() + ->scalarNode('src') + ->info('The path to the screenshot.') + ->example('screenshot/lowres.webp') + ->end() + ->scalarNode('path') + ->info('The path to an application page. The screenshot will be generated.') + ->example('https://example.com') + ->end() + ->scalarNode('height') + ->defaultNull() + ->info('When using "route" or "path", the height of the screenshot.') + ->example('1080') + ->end() + ->scalarNode('width') + ->defaultNull() + ->info('When using "route" or "path", the height of the screenshot.') + ->example('1080') + ->end() + ->scalarNode('form_factor') + ->info('The form factor of the screenshot. Will guess the form factor if not set.') + ->example(['wide', 'narrow']) + ->end() + ->scalarNode('label') + ->info('The label of the screenshot.') + ->example('Homescreen of Awesome App') + ->end() + ->scalarNode('platform') + ->info('The platform of the screenshot.') + ->example( + ['android', 'windows', 'chromeos', 'ipados', 'ios', 'kaios', 'macos', 'windows', 'xbox'] + ) + ->end() + ->scalarNode('format') + ->info('The format of the screenshot. Will convert the file if set.') + ->example(['jpg', 'png', 'webp']) + ->end() + ->end() + ->end(); + + return $node; + } } diff --git a/src/DependencyInjection/SpomkyLabsPwaExtension.php b/src/DependencyInjection/SpomkyLabsPwaExtension.php index e4cbefd..0405a6d 100644 --- a/src/DependencyInjection/SpomkyLabsPwaExtension.php +++ b/src/DependencyInjection/SpomkyLabsPwaExtension.php @@ -31,7 +31,10 @@ public function load(array $configs, ContainerBuilder $container): void if ($config['image_processor'] !== null) { $container->setAlias(ImageProcessor::class, $config['image_processor']); } - unset($config['image_processor']); + if ($config['web_client'] !== null) { + $container->setAlias('pwa.web_client', $config['web_client']); + } + unset($config['image_processor'], $config['web_client']); $params = [ 'icon_folder', 'icon_prefix_url', diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 27df479..4b2f8ef 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -3,7 +3,6 @@ declare(strict_types=1); use SpomkyLabs\PwaBundle\Command\GenerateManifestCommand; -use SpomkyLabs\PwaBundle\Command\WorkboxInitCommand; use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor; use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -17,7 +16,6 @@ ; $container->set(GenerateManifestCommand::class); - $container->set(WorkboxInitCommand::class); if (extension_loaded('imagick')) { $container