feat(pwa): add service-worker + webmanifest for each podcasts to have them install on devices

- configure service-worker using vite-plugin-pwa
- refactor Image entity to generate images of
different types based on size config
- add requirement for webp library for php gd to generate webp
images for instance
- add action to regenerate all instance images for eventual Images config
changes
- enhance google lighthouse metrics for pwa
This commit is contained in:
Yassine Doghri 2021-11-23 11:54:34 +00:00
parent 902f959b30
commit fee2c1c0d0
80 changed files with 5419 additions and 195 deletions

View File

@ -37,6 +37,7 @@ RUN apt-get update \
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image) # https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
libicu-dev \ libicu-dev \
libpng-dev \ libpng-dev \
libwebp-dev \
libjpeg-dev \ libjpeg-dev \
zlib1g-dev \ zlib1g-dev \
libzip-dev \ libzip-dev \
@ -44,7 +45,7 @@ RUN apt-get update \
&& docker-php-ext-install intl \ && docker-php-ext-install intl \
&& docker-php-ext-install zip \ && docker-php-ext-install zip \
# gd for image processing # gd for image processing
&& docker-php-ext-configure gd --with-jpeg \ && docker-php-ext-configure gd --with-webp --with-jpeg \
&& docker-php-ext-install gd \ && docker-php-ext-install gd \
# redis extension for cache # redis extension for cache
&& pecl install -o -f redis \ && pecl install -o -f redis \

View File

@ -69,7 +69,8 @@ PHP version 8.0 or higher is required, with the following extensions installed:
- [intl](https://php.net/manual/en/intl.requirements.php) - [intl](https://php.net/manual/en/intl.requirements.php)
- [libcurl](https://php.net/manual/en/curl.requirements.php) - [libcurl](https://php.net/manual/en/curl.requirements.php)
- [mbstring](https://php.net/manual/en/mbstring.installation.php) - [mbstring](https://php.net/manual/en/mbstring.installation.php)
- [gd](https://www.php.net/manual/en/image.installation.php) - [gd](https://www.php.net/manual/en/image.installation.php) with **JPEG**,
**PNG** and **WEBP** libraries.
Additionally, make sure that the following extensions are enabled in your PHP: Additionally, make sure that the following extensions are enabled in your PHP:

View File

@ -47,15 +47,57 @@ class Images extends BaseConfig
* *
* Array values are as follows: 'name' => [width, height] * Array values are as follows: 'name' => [width, height]
* *
* @var array<string, int[]> * @var array<string, array<string, int|string>>
*/ */
public array $podcastCoverSizes = [ public array $podcastCoverSizes = [
'tiny' => [40, 40], 'tiny' => [
'thumbnail' => [150, 150], 'width' => 40,
'medium' => [320, 320], 'height' => 40,
'large' => [1024, 1024], 'mimetype' => 'image/webp',
'feed' => [1400, 1400], 'extension' => 'webp',
'id3' => [500, 500], ],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'large' => [
'width' => 1024,
'height' => 1024,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'feed' => [
'width' => 1400,
'height' => 1400,
],
'id3' => [
'width' => 500,
'height' => 500,
],
'federation' => [
'width' => 400,
'height' => 400,
],
'webmanifest192' => [
'width' => 192,
'height' => 192,
'mimetype' => 'image/png',
'extension' => 'png',
],
'webmanifest512' => [
'width' => 512,
'height' => 512,
'mimetype' => 'image/png',
'extension' => 'png',
],
]; ];
/** /**
@ -63,14 +105,25 @@ class Images extends BaseConfig
* *
* Uploaded podcast header covers are of 3:1 ratio * Uploaded podcast header covers are of 3:1 ratio
* *
* Array values are as follows: 'name' => [width, height] * @var array<string, array<string, int|string>>
*
* @var array<string, int[]>
*/ */
public array $podcastBannerSizes = [ public array $podcastBannerSizes = [
'small' => [320, 128], 'small' => [
'medium' => [960, 320], 'width' => 320,
'large' => [1500, 500], 'height' => 128,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 960,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'federation' => [
'width' => 1500,
'height' => 500,
],
]; ];
public string $podcastBannerDefaultPath = 'castopod-banner-default.jpg'; public string $podcastBannerDefaultPath = 'castopod-banner-default.jpg';
@ -84,11 +137,27 @@ class Images extends BaseConfig
* *
* Array values are as follows: 'name' => [width, height] * Array values are as follows: 'name' => [width, height]
* *
* @var array<string, int[]> * @var array<string, array<string, int|string>>
*/ */
public array $personAvatarSizes = [ public array $personAvatarSizes = [
'tiny' => [40, 40], 'tiny' => [
'thumbnail' => [150, 150], 'width' => 40,
'medium' => [320, 320], 'height' => 40,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' =>
'image/webp',
'extension' => 'webp',
],
]; ];
} }

View File

@ -65,6 +65,10 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
$routes->get('/', 'PodcastController::activity/$1', [ $routes->get('/', 'PodcastController::activity/$1', [
'as' => 'podcast-activity', 'as' => 'podcast-activity',
]); ]);
$routes->get('manifest.webmanifest', 'WebmanifestController::podcastManifest/$1', [
'as' => 'podcast-webmanifest',
]);
// override default Fediverse Library's actor route // override default Fediverse Library's actor route
$routes->options('/', 'ActivityPubController::preflight'); $routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'PodcastController::activity/$1', [ $routes->get('/', 'PodcastController::activity/$1', [

View File

@ -204,9 +204,9 @@ class EpisodeController extends BaseController
'height' => 144, 'height' => 144,
'thumbnail_url' => $this->episode->cover->large_url, 'thumbnail_url' => $this->episode->cover->large_url,
'thumbnail_width' => config('Images') 'thumbnail_width' => config('Images')
->podcastCoverSizes['large'][0], ->podcastCoverSizes['large']['width'],
'thumbnail_height' => config('Images') 'thumbnail_height' => config('Images')
->podcastCoverSizes['large'][1], ->podcastCoverSizes['large']['height'],
]); ]);
} }
@ -222,8 +222,8 @@ class EpisodeController extends BaseController
$oembed->addChild('author_name', $this->podcast->title); $oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link); $oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->cover->large_url); $oembed->addChild('thumbnail', $this->episode->cover->large_url);
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['large'][0]); $oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['large']['width']);
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['large'][1]); $oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['large']['height']);
$oembed->addChild( $oembed->addChild(
'html', 'html',
htmlentities( htmlentities(

View File

@ -10,11 +10,44 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Models\PodcastModel;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
class WebmanifestController extends Controller class WebmanifestController extends Controller
{ {
/**
* @var array<string, string>
*/
public const THEME_COLORS = [
'pine' => [
'theme' => '#009486',
'background' => '#F0F9F8',
],
'lake' => [
'theme' => '#00ACE0',
'background' => '#F0F7F9',
],
'jacaranda' => [
'theme' => '#562CDD',
'background' => '#F2F0F9',
],
'crimson' => [
'theme' => '#F24562',
'background' => '#F9F0F2',
],
'amber' => [
'theme' => '#FF6224',
'background' => '#F9F3F0',
],
'onyx' => [
'theme' =>
'#040406',
'background' => '#F3F3F7',
],
];
public function index(): ResponseInterface public function index(): ResponseInterface
{ {
$webmanifest = [ $webmanifest = [
@ -22,8 +55,13 @@ class WebmanifestController extends Controller
->get('App.siteName'), ->get('App.siteName'),
'description' => service('settings') 'description' => service('settings')
->get('App.siteDescription'), ->get('App.siteDescription'),
'lang' => service('request')
->getLocale(),
'start_url' => base_url(),
'display' => 'minimal-ui', 'display' => 'minimal-ui',
'theme_color' => '#009486', 'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [ 'icons' => [
[ [
'src' => service('settings') 'src' => service('settings')
@ -42,4 +80,39 @@ class WebmanifestController extends Controller
return $this->response->setJSON($webmanifest); return $this->response->setJSON($webmanifest);
} }
public function podcastManifest(string $podcastHandle): ResponseInterface
{
if (
($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$webmanifest = [
'name' => $podcast->title,
'short_name' => '@' . $podcast->handle,
'description' => $podcast->description,
'lang' => $podcast->language_code,
'start_url' => $podcast->link,
'display' => 'minimal-ui',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'sizes' => '192x192',
],
[
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
} }

View File

@ -41,4 +41,22 @@ class Actor extends FediverseActor
return $this->podcast; return $this->podcast;
} }
public function getAvatarImageUrl(): string
{
if ($this->podcast !== null) {
return $this->podcast->cover->thumbnail_url;
}
return $this->attributes['avatar_image_url'];
}
public function getAvatarImageMimetype(): string
{
if ($this->podcast !== null) {
return $this->podcast->cover->thumbnail_mimetype;
}
return $this->attributes['avatar_image_mimetype'];
}
} }

View File

@ -200,7 +200,9 @@ class Episode extends Entity
public function getCover(): Image public function getCover(): Image
{ {
if ($coverPath = $this->attributes['cover_path']) { if ($coverPath = $this->attributes['cover_path']) {
return new Image(null, $coverPath, $this->attributes['cover_mimetype']); return new Image(null, $coverPath, $this->attributes['cover_mimetype'], config(
'Images'
)->podcastCoverSizes);
} }
return $this->getPodcast() return $this->getPodcast()

View File

@ -28,7 +28,7 @@ class Image extends Entity
{ {
protected Images $config; protected Images $config;
protected ?File $file = null; protected File $file;
protected string $dirname; protected string $dirname;
@ -38,7 +38,16 @@ class Image extends Entity
protected string $mimetype; protected string $mimetype;
public function __construct(?File $file, string $path = '', string $mimetype = '') /**
* @var array<string, array<string, int|string>>
*/
protected array $sizes = [];
/**
* @param array<string, array<string, int|string>> $sizes
* @param File $file
*/
public function __construct(?File $file, string $path = '', string $mimetype = '', array $sizes = [])
{ {
if ($file === null && $path === '') { if ($file === null && $path === '') {
throw new RuntimeException('File or path must be set to create an Image.'); throw new RuntimeException('File or path must be set to create an Image.');
@ -63,11 +72,17 @@ class Image extends Entity
] = pathinfo($path); ] = pathinfo($path);
} }
if ($file === null) {
helper('media');
$file = new File(media_path($path));
}
$this->file = $file; $this->file = $file;
$this->dirname = $dirname; $this->dirname = $dirname;
$this->filename = $filename; $this->filename = $filename;
$this->extension = $extension; $this->extension = $extension;
$this->mimetype = $mimetype; $this->mimetype = $mimetype;
$this->sizes = $sizes;
} }
public function __get($property) public function __get($property)
@ -91,7 +106,24 @@ class Image extends Entity
if ($this->dirname !== '.') { if ($this->dirname !== '.') {
$path .= $this->dirname . '/'; $path .= $this->dirname . '/';
} }
$path .= $this->filename . $fileSuffix . '.' . $this->extension; $path .= $this->filename . $fileSuffix;
$extension = '.' . $this->extension;
$mimetype = $this->mimetype;
if ($fileSuffix !== '') {
$sizeName = substr($fileSuffix, 1);
if (array_key_exists('extension', $this->sizes[$sizeName])) {
$extension = '.' . $this->sizes[$sizeName]['extension'];
}
if (array_key_exists('mimetype', $this->sizes[$sizeName])) {
$mimetype = $this->sizes[$sizeName]['mimetype'];
}
}
$path .= $extension;
if (str_ends_with($property, 'mimetype')) {
return $mimetype;
}
if (str_ends_with($property, 'url')) { if (str_ends_with($property, 'url')) {
helper('media'); helper('media');
@ -111,15 +143,11 @@ class Image extends Entity
public function getFile(): File public function getFile(): File
{ {
if ($this->file === null) {
$this->file = new File($this->path);
}
return $this->file; return $this->file;
} }
/** /**
* @param array<string, int[]> $sizes * @param array<string, array<string, int|string>> $sizes
*/ */
public function saveImage(array $sizes, string $dirname, string $filename): void public function saveImage(array $sizes, string $dirname, string $filename): void
{ {
@ -127,6 +155,7 @@ class Image extends Entity
$this->dirname = $dirname; $this->dirname = $dirname;
$this->filename = $filename; $this->filename = $filename;
$this->sizes = $sizes;
save_media($this->file, $this->dirname, $this->filename); save_media($this->file, $this->dirname, $this->filename);
@ -136,8 +165,8 @@ class Image extends Entity
$pathProperty = $name . '_path'; $pathProperty = $name . '_path';
$imageService $imageService
->withFile(media_path($this->path)) ->withFile(media_path($this->path))
->resize($size[0], $size[1]) ->resize($size['width'], $size['height']);
->save(media_path($this->{$pathProperty})); $imageService->save(media_path($this->{$pathProperty}));
} }
} }

View File

@ -77,10 +77,12 @@ class Person extends Entity
public function getAvatar(): Image public function getAvatar(): Image
{ {
if ($this->attributes['avatar_path'] === null) { if ($this->attributes['avatar_path'] === null) {
return new Image(null, '/castopod-avatar-default.jpg', 'image/jpeg'); return new Image(null, '/castopod-avatar-default.jpg', 'image/jpeg', config('Images')->personAvatarSizes);
} }
return new Image(null, $this->attributes['avatar_path'], $this->attributes['avatar_mimetype']); return new Image(null, $this->attributes['avatar_path'], $this->attributes['avatar_mimetype'], config(
'Images'
)->personAvatarSizes);
} }
/** /**

View File

@ -211,7 +211,7 @@ class Podcast extends Entity
public function getCover(): Image public function getCover(): Image
{ {
return new Image(null, $this->cover_path, $this->cover_mimetype); return new Image(null, $this->cover_path, $this->cover_mimetype, config('Images')->podcastCoverSizes);
} }
/** /**
@ -248,11 +248,13 @@ class Podcast extends Entity
config('Images') config('Images')
->podcastBannerDefaultPath, ->podcastBannerDefaultPath,
config('Images') config('Images')
->podcastBannerDefaultMimeType ->podcastBannerDefaultMimeType,
config('Images')
->podcastBannerSizes
); );
} }
return new Image(null, $this->banner_path, $this->banner_mimetype); return new Image(null, $this->banner_path, $this->banner_mimetype, config('Images') ->podcastBannerSizes);
} }
public function getLink(): string public function getLink(): string

View File

@ -24,7 +24,7 @@ if (! function_exists('get_podcast_metatags')) {
$schema = new Schema( $schema = new Schema(
new Thing('PodcastSeries', [ new Thing('PodcastSeries', [
'name' => $podcast->title, 'name' => $podcast->title,
'url' => url_to('podcast-activity', $podcast->handle), 'url' => $podcast->link,
'image' => $podcast->cover->feed_url, 'image' => $podcast->cover->feed_url,
'description' => $podcast->description, 'description' => $podcast->description,
'webFeed' => $podcast->feed_url, 'webFeed' => $podcast->feed_url,
@ -41,8 +41,8 @@ if (! function_exists('get_podcast_metatags')) {
->description(htmlspecialchars($podcast->description)) ->description(htmlspecialchars($podcast->description))
->image((string) $podcast->cover->large_url) ->image((string) $podcast->cover->large_url)
->canonical((string) current_url()) ->canonical((string) current_url())
->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0]) ->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1]) ->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
->og('locale', $podcast->language_code) ->og('locale', $podcast->language_code)
->og('site_name', service('settings')->get('App.siteName')); ->og('site_name', service('settings')->get('App.siteName'));
@ -70,7 +70,7 @@ if (! function_exists('get_episode_metatags')) {
]), ]),
'partOfSeries' => new Thing('PodcastSeries', [ 'partOfSeries' => new Thing('PodcastSeries', [
'name' => $episode->podcast->title, 'name' => $episode->podcast->title,
'url' => url_to('podcast-activity', $episode->podcast->handle), 'url' => $episode->podcast->link,
]), ]),
]) ])
); );
@ -83,8 +83,8 @@ if (! function_exists('get_episode_metatags')) {
->image((string) $episode->cover->large_url, 'player') ->image((string) $episode->cover->large_url, 'player')
->canonical($episode->link) ->canonical($episode->link)
->og('site_name', service('settings')->get('App.siteName')) ->og('site_name', service('settings')->get('App.siteName'))
->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0]) ->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1]) ->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
->og('locale', $episode->podcast->language_code) ->og('locale', $episode->podcast->language_code)
->og('audio', $episode->audio_file_opengraph_url) ->og('audio', $episode->audio_file_opengraph_url)
->og('audio:type', $episode->audio_file_mimetype) ->og('audio:type', $episode->audio_file_mimetype)

View File

@ -485,9 +485,9 @@ class PodcastModel extends Model
// update values // update values
$actor->display_name = $podcast->title; $actor->display_name = $podcast->title;
$actor->summary = $podcast->description_html; $actor->summary = $podcast->description_html;
$actor->avatar_image_url = $podcast->cover->thumbnail_url; $actor->avatar_image_url = $podcast->cover->federation_url;
$actor->avatar_image_mimetype = $podcast->cover->mimetype; $actor->avatar_image_mimetype = $podcast->cover->mimetype;
$actor->cover_image_url = $podcast->banner->large_url; $actor->cover_image_url = $podcast->banner->federation_url;
$actor->cover_image_mimetype = $podcast->banner->mimetype; $actor->cover_image_mimetype = $podcast->banner->mimetype;
if ($actor->hasChanged()) { if ($actor->hasChanged()) {

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm4.82-4.924A7 7 0 0 0 9.032 5.658l.975 1.755A5 5 0 0 1 17 12h-3l2.82 5.076zm-1.852 1.266l-.975-1.755A5 5 0 0 1 7 12h3L7.18 6.924a7 7 0 0 0 7.788 11.418z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 377 B

6
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c0a25c3d11c806b4bc62eafb22902bc8", "content-hash": "afb6585b90ed08cc8a257f346ab1c416",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -603,8 +603,8 @@
"php": ">=5.4.0" "php": ">=5.4.0"
}, },
"require-dev": { "require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.0", "jakub-onderka/php-parallel-lint": "^0.9 || ^1.0",
"phpunit/phpunit": "^4.8 || ^5.0 || ^6.1 || ^7.5 || ^8.5" "phpunit/phpunit": "^4.8|^5.0"
}, },
"suggest": { "suggest": {
"ext-SimpleXML": "SimpleXML extension is required to analyze RIFF/WAV/BWF audio files (also requires `ext-libxml`).", "ext-SimpleXML": "SimpleXML extension is required to analyze RIFF/WAV/BWF audio files (also requires `ext-libxml`).",

View File

@ -153,8 +153,8 @@ You do not wish to use the VSCode devcontainer? No problem!
> The `docker-compose up -d` command will boot 4 containers in the > The `docker-compose up -d` command will boot 4 containers in the
> background: > background:
> >
> - `castopod-host_app`: a php based container with CodeIgniter4 requirements > - `castopod-host_app`: a php based container with Castopod Host
> installed > requirements installed
> - `castopod-host_redis`: a [redis](https://redis.io/) database to handle > - `castopod-host_redis`: a [redis](https://redis.io/) database to handle
> queries and pages caching > queries and pages caching
> - `castopod-host_mariadb`: a [mariadb](https://mariadb.org/) server for > - `castopod-host_mariadb`: a [mariadb](https://mariadb.org/) server for

View File

@ -31,6 +31,10 @@ $routes->group(
'as' => 'settings-instance-delete-icon', 'as' => 'settings-instance-delete-icon',
'filter' => 'permission:settings-manage', 'filter' => 'permission:settings-manage',
]); ]);
$routes->post('instance-images-regenerate', 'SettingsController::regenerateImages', [
'as' => 'settings-images-regenerate',
'filter' => 'permission:settings-manage',
]);
$routes->get('theme', 'SettingsController::theme', [ $routes->get('theme', 'SettingsController::theme', [
'as' => 'settings-theme', 'as' => 'settings-theme',
'filter' => 'permission:settings-manage', 'filter' => 'permission:settings-manage',

View File

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace Modules\Admin\Controllers; namespace Modules\Admin\Controllers;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use PHP_ICO; use PHP_ICO;
@ -75,20 +77,20 @@ class SettingsController extends BaseController
service('image') service('image')
->withFile(ROOTPATH . 'public/media/site/icon.png') ->withFile(ROOTPATH . 'public/media/site/icon.png')
->resize($size, $size) ->resize($size, $size)
->save(ROOTPATH . "public/media/site/icon-{$size}.{$randomHash}.png"); ->save(media_path("/site/icon-{$size}.{$randomHash}.png"));
} }
service('settings') service('settings')
->set('App.siteIcon', [ ->set('App.siteIcon', [
'ico' => "/media/site/favicon.{$randomHash}.ico", 'ico' => media_path("/site/favicon.{$randomHash}.ico"),
'64' => "/media/site/icon-64.{$randomHash}.png", '64' => media_path("/site/icon-64.{$randomHash}.png"),
'180' => "/media/site/icon-180.{$randomHash}.png", '180' => media_path("/site/icon-180.{$randomHash}.png"),
'192' => "/media/site/icon-192.{$randomHash}.png", '192' => media_path("/site/icon-192.{$randomHash}.png"),
'512' => "/media/site/icon-512.{$randomHash}.png", '512' => media_path("/site/icon-512.{$randomHash}.png"),
]); ]);
} }
return redirect('settings-general')->with('message', lang('Settings.general.instanceEditSuccess')); return redirect('settings-general')->with('message', lang('Settings.instance.editSuccess'));
} }
public function deleteIcon(): RedirectResponse public function deleteIcon(): RedirectResponse
@ -100,7 +102,52 @@ class SettingsController extends BaseController
service('settings') service('settings')
->forget('App.siteIcon'); ->forget('App.siteIcon');
return redirect('settings-general')->with('message', lang('Settings.general.deleteIconSuccess')); return redirect('settings-general')->with('message', lang('Settings.instance.deleteIconSuccess'));
}
public function regenerateImages(): RedirectResponse
{
$allPodcasts = (new PodcastModel())->findAll();
foreach ($allPodcasts as $podcast) {
$podcastImages = glob(ROOTPATH . "public/media/podcasts/{$podcast->handle}/*_*");
if ($podcastImages) {
foreach ($podcastImages as $podcastImage) {
if (is_file($podcastImage)) {
unlink($podcastImage);
}
}
}
$podcast->setCover($podcast->cover);
if ($podcast->banner_path !== null) {
$podcast->setBanner($podcast->banner);
}
foreach ($podcast->episodes as $episode) {
if ($episode->cover_path !== null) {
$episode->setCover($episode->cover);
}
}
}
$personsImages = glob(ROOTPATH . 'public/media/persons/*_*');
if ($personsImages) {
foreach ($personsImages as $personsImage) {
if (is_file($personsImage)) {
unlink($personsImage);
}
}
}
$persons = (new PersonModel())->findAll();
foreach ($persons as $person) {
if ($person->avatar_path !== null) {
$person->setAvatar($person->avatar);
}
}
return redirect('settings-general')->with('message', lang('Settings.images.regenerationSuccess'));
} }
public function theme(): string public function theme(): string

View File

@ -9,6 +9,7 @@ declare(strict_types=1);
*/ */
return [ return [
'toggle_sidebar' => 'Toggle sidebar',
'go_to_website' => 'View site', 'go_to_website' => 'View site',
'go_to_admin' => 'Go to admin', 'go_to_admin' => 'Go to admin',
'dashboard' => 'Dashboard', 'dashboard' => 'Dashboard',

View File

@ -10,8 +10,8 @@ declare(strict_types=1);
return [ return [
'title' => 'General settings', 'title' => 'General settings',
'general' => [ 'instance' => [
'site_section_title' => 'Instance', 'title' => 'Instance',
'site_icon' => 'Site icon', 'site_icon' => 'Site icon',
'site_icon_delete' => 'Delete site icon', 'site_icon_delete' => 'Delete site icon',
'site_icon_hint' => 'Site icons are what you see on your browser tabs, bookmarks bar, and when you add a website as a shortcut on mobile devices.', 'site_icon_hint' => 'Site icons are what you see on your browser tabs, bookmarks bar, and when you add a website as a shortcut on mobile devices.',
@ -19,9 +19,15 @@ return [
'site_name' => 'Site name', 'site_name' => 'Site name',
'site_description' => 'Site description', 'site_description' => 'Site description',
'submit' => 'Save', 'submit' => 'Save',
'instanceEditSuccess' => 'Instance has been updated successfully!', 'editSuccess' => 'Instance has been updated successfully!',
'deleteIconSuccess' => 'Site icon has been remove successfully!', 'deleteIconSuccess' => 'Site icon has been remove successfully!',
], ],
'images' => [
'title' => 'Images',
'subtitle' => 'Here you can regenerate all images based on the originals that were uploaded.',
'regenerate' => 'Regenerate images',
'regenerationSuccess' => 'All images have been regenerated successfully!',
],
'theme' => [ 'theme' => [
'title' => 'Theme', 'title' => 'Theme',
'accent_section_title' => 'Accent color', 'accent_section_title' => 'Accent color',

View File

@ -9,6 +9,7 @@ declare(strict_types=1);
*/ */
return [ return [
'toggle_sidebar' => 'Afficher ou cacher la barre latérale',
'go_to_website' => 'Visiter le site', 'go_to_website' => 'Visiter le site',
'dashboard' => 'Tableau de bord', 'dashboard' => 'Tableau de bord',
'admin' => 'Accueil', 'admin' => 'Accueil',

View File

@ -10,8 +10,8 @@ declare(strict_types=1);
return [ return [
'title' => 'Paramètres généraux', 'title' => 'Paramètres généraux',
'general' => [ 'instance' => [
'site_section_title' => 'Instance', 'title' => 'Instance',
'site_icon' => 'Favicon du site', 'site_icon' => 'Favicon du site',
'site_icon_delete' => 'Supprimer la favicon du site', 'site_icon_delete' => 'Supprimer la favicon du site',
'site_icon_hint' => 'Les favicons sont ce que vous voyez sur les onglets de votre navigateur, dans votre barre de favoris, et lorsque vous ajoutez un site web en raccourci sur des appareils mobiles.', 'site_icon_hint' => 'Les favicons sont ce que vous voyez sur les onglets de votre navigateur, dans votre barre de favoris, et lorsque vous ajoutez un site web en raccourci sur des appareils mobiles.',
@ -19,9 +19,15 @@ return [
'site_name' => 'Titre du site', 'site_name' => 'Titre du site',
'site_description' => 'Description du site', 'site_description' => 'Description du site',
'submit' => 'Sauvegarder', 'submit' => 'Sauvegarder',
'instanceEditSuccess' => 'Linstance a bien été mise à jour!', 'editSuccess' => 'Linstance a bien été mise à jour!',
'deleteIconSuccess' => 'La favicon du site a bien été retirée!', 'deleteIconSuccess' => 'La favicon du site a bien été retirée!',
], ],
'images' => [
'title' => 'Images',
'subtitle' => 'Vous pouvez ici regénérer toutes les images en se basant sur celles qui ont été téléversées à lorigine.',
'regenerate' => 'Regénérer les images',
'regenerationSuccess' => 'Toutes les images ont été regénérés avec succès!',
],
'theme' => [ 'theme' => [
'title' => 'Thème', 'title' => 'Thème',
'accent_section_title' => 'Couleur daccentuation', 'accent_section_title' => 'Couleur daccentuation',
@ -32,6 +38,7 @@ return [
'lake' => 'Lac', 'lake' => 'Lac',
'jacaranda' => 'Jacaranda', 'jacaranda' => 'Jacaranda',
'onyx' => 'Onyx', 'onyx' => 'Onyx',
'submit' => 'Sauvegarder',
'setInstanceThemeSuccess' => 'Le thème a bien été mis à jour!', 'setInstanceThemeSuccess' => 'Le thème a bien été mis à jour!',
], ],
]; ];

4980
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -83,7 +83,12 @@
"svgo": "^2.8.0", "svgo": "^2.8.0",
"tailwindcss": "^3.0.0-alpha.1", "tailwindcss": "^3.0.0-alpha.1",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"vite": "^2.6.13" "vite": "^2.6.13",
"vite-plugin-pwa": "^0.11.5",
"workbox-build": "^6.4.0",
"workbox-core": "^6.4.0",
"workbox-routing": "^6.4.0",
"workbox-strategies": "^6.4.0"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,css,md,json}": "prettier --write", "*.{js,ts,css,md,json}": "prettier --write",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -52,6 +52,7 @@ module.exports = {
subtle: withOpacity("--color-border-subtle"), subtle: withOpacity("--color-border-subtle"),
contrast: withOpacity("--color-border-contrast"), contrast: withOpacity("--color-border-contrast"),
navigation: withOpacity("--color-border-navigation"), navigation: withOpacity("--color-border-navigation"),
"navigation-bg": withOpacity("--color-background-navigation"),
accent: { accent: {
base: withOpacity("--color-accent-base"), base: withOpacity("--color-accent-base"),
hover: withOpacity("--color-accent-hover"), hover: withOpacity("--color-accent-hover"),

View File

@ -1,7 +1,7 @@
<header class="sticky top-0 z-[60] flex items-center h-10 text-white border-b col-span-full bg-navigation border-navigation"> <header class="sticky top-0 z-[60] flex items-center h-10 text-white border-b col-span-full bg-navigation border-navigation">
<button type="button" <button type="button"
data-sidebar-toggler="toggler" data-sidebar-toggler="toggler"
class="h-full pr-1 text-xl md:hidden focus:ring-accent focus:ring-inset"><?= icon('menu') ?></button> class="h-full pr-1 text-xl md:hidden focus:ring-accent focus:ring-inset" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu') ?></button>
<div class="inline-flex items-center h-full"> <div class="inline-flex items-center h-full">
<a href="<?= route_to( <a href="<?= route_to(
'admin', 'admin',
@ -25,7 +25,7 @@
aria-expanded="false"><div class="relative mr-1"> aria-expanded="false"><div class="relative mr-1">
<?= icon('account-circle', 'text-3xl opacity-60') ?> <?= icon('account-circle', 'text-3xl opacity-60') ?>
<?= user() <?= user()
->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 rounded-full -right-1" />' ?> ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" />' ?>
</div> </div>
<?= user()->username ?> <?= user()->username ?>
<?= icon('caret-down', 'ml-auto text-2xl') ?></button> <?= icon('caret-down', 'ml-auto text-2xl') ?></button>

View File

@ -2,7 +2,7 @@
<a href="<?= route_to('episode-view', $episode->podcast->id, $episode->id) ?>" class="flex flex-col justify-end w-full h-full text-white group"> <a href="<?= route_to('episode-view', $episode->podcast->id, $episode->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div> <div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden"> <div class="w-full h-full overflow-hidden">
<img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" /> <img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105 aspect-square" />
</div> </div>
<?= publication_pill($episode->published_at, $episode->publication_status, 'absolute top-0 left-0 ml-2 mt-2 text-sm'); ?> <?= publication_pill($episode->published_at, $episode->publication_status, 'absolute top-0 left-0 ml-2 mt-2 text-sm'); ?>
<div class="absolute z-20 flex flex-col items-start px-4 py-2"> <div class="absolute z-20 flex flex-col items-start px-4 py-2">

View File

@ -12,7 +12,7 @@ $podcastNavigation = [
<img <img
src="<?= $podcast->cover->tiny_url ?>" src="<?= $podcast->cover->tiny_url ?>"
alt="<?= $podcast->title ?>" alt="<?= $podcast->title ?>"
class="object-cover w-6 h-6 rounded" class="object-cover w-6 h-6 rounded aspect-square"
/> />
<span class="flex-1 w-full px-2 text-xs font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span> <span class="flex-1 w-full px-2 text-xs font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span>
</a> </a>
@ -20,7 +20,7 @@ $podcastNavigation = [
<img <img
src="<?= $episode->cover->thumbnail_url ?>" src="<?= $episode->cover->thumbnail_url ?>"
alt="<?= $episode->title ?>" alt="<?= $episode->title ?>"
class="object-cover w-16 h-16 rounded" class="object-cover w-16 h-16 rounded aspect-square"
/> />
<div class="flex flex-col items-start flex-1 w-48 px-2"> <div class="flex flex-col items-start flex-1 w-48 px-2">
<span class="w-full font-semibold truncate" title="<?= $episode->title ?>"><?= $episode->title ?></span> <span class="w-full font-semibold truncate" title="<?= $episode->title ?>"><?= $episode->title ?></span>

View File

@ -34,7 +34,7 @@
$episode->audio_file_duration, $episode->audio_file_duration,
) . ) .
'</time>' . '</time>' .
'<img loading="lazy" src="' . $episode->cover->thumbnail_url . '" alt="' . $episode->title . '" class="object-cover w-20 h-20 rounded-lg" />' . '<img loading="lazy" src="' . $episode->cover->thumbnail_url . '" alt="' . $episode->title . '" class="object-cover w-20 rounded-lg aspect-square" />' .
'</div>' . '</div>' .
'<a class="overflow-x-hidden text-sm hover:underline" href="' . route_to( '<a class="overflow-x-hidden text-sm hover:underline" href="' . route_to(
'episode-view', 'episode-view',

View File

@ -58,7 +58,7 @@
return '<div class="flex">' . return '<div class="flex">' .
'<a href="' . '<a href="' .
route_to('person-view', $person->id) . route_to('person-view', $person->id) .
"\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" . "\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 rounded-full aspect-square\" /></a>" .
'<div class="flex flex-col ml-3">' . '<div class="flex flex-col ml-3">' .
$person->full_name . $person->full_name .
implode( implode(

View File

@ -28,7 +28,7 @@
<small class="max-w-md mb-2 text-skin-muted"><?= lang('Episode.publish_form.post_hint') ?></small> <small class="max-w-md mb-2 text-skin-muted"><?= lang('Episode.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl"> <div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl">
<div class="flex px-4 py-3 gap-x-2"> <div class="flex px-4 py-3 gap-x-2">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-10 h-10 rounded-full" /> <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<p class="flex items-baseline min-w-0"> <p class="flex items-baseline min-w-0">
<span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span> <span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span>
@ -41,7 +41,7 @@
</div> </div>
<div class="flex border-y"> <div class="flex border-y">
<img src="<?= $episode->cover <img src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" /> ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24 aspect-square" />
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2"> <a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">
<div class="flex items-baseline"> <div class="flex items-baseline">

View File

@ -29,7 +29,7 @@
<small class="max-w-md mb-2 text-skin-muted"><?= lang('Episode.publish_form.post_hint') ?></small> <small class="max-w-md mb-2 text-skin-muted"><?= lang('Episode.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl"> <div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl">
<div class="flex px-4 py-3 gap-x-2"> <div class="flex px-4 py-3 gap-x-2">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-10 h-10 rounded-full" /> <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<p class="flex items-baseline min-w-0"> <p class="flex items-baseline min-w-0">
<span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span> <span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span>
@ -43,7 +43,7 @@
</div> </div>
<div class="flex border-y"> <div class="flex border-y">
<img src="<?= $episode->cover <img src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" /> ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24 aspect-square" />
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2"> <a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">
<div class="flex items-baseline"> <div class="flex items-baseline">

View File

@ -2,7 +2,7 @@
<a href="<?= route_to('person-view', $person->id) ?>" class="flex flex-col justify-end w-full h-full text-white group"> <a href="<?= route_to('person-view', $person->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div> <div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden"> <div class="w-full h-full overflow-hidden">
<img alt="<?= $person->full_name ?>" src="<?= $person->avatar->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" /> <img alt="<?= $person->full_name ?>" src="<?= $person->avatar->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" />
</div> </div>
<div class="absolute z-20"> <div class="absolute z-20">
<h2 class="px-4 py-2 font-semibold leading-tight"><?= $person->full_name ?></h2> <h2 class="px-4 py-2 font-semibold leading-tight"><?= $person->full_name ?></h2>

View File

@ -19,7 +19,7 @@
<img <img
src="<?= $person->avatar->medium_url ?>" src="<?= $person->avatar->medium_url ?>"
alt="$person->full_name" alt="$person->full_name"
class="object-cover w-full max-w-xs rounded" class="object-cover w-full max-w-xs rounded aspect-square"
/> />
<div class="flex flex-col"> <div class="flex flex-col">
<?= $person->full_name ?> <?= $person->full_name ?>

View File

@ -4,7 +4,7 @@
<div class="w-full h-full overflow-hidden"> <div class="w-full h-full overflow-hidden">
<img <img
alt="<?= $podcast->title ?>" alt="<?= $podcast->title ?>"
src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" /> src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" />
</div> </div>
<div class="absolute z-20 px-4 pb-4 transition duration-75 ease-out translate-y-6 group-focus:translate-y-0 group-hover:translate-y-0"> <div class="absolute z-20 px-4 pb-4 transition duration-75 ease-out translate-y-6 group-focus:translate-y-0 group-hover:translate-y-0">
<h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2> <h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2>

View File

@ -39,7 +39,7 @@ $podcastNavigation = [
<img <img
src="<?= $podcast->cover->thumbnail_url ?>" src="<?= $podcast->cover->thumbnail_url ?>"
alt="<?= $podcast->title ?>" alt="<?= $podcast->title ?>"
class="object-cover w-16 h-16 rounded" class="object-cover w-16 h-16 rounded aspect-square"
/> />
<div class="flex flex-col items-start flex-1 w-48 px-2"> <div class="flex flex-col items-start flex-1 w-48 px-2">
<span class="w-full font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span> <span class="w-full font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span>

View File

@ -28,7 +28,7 @@
<img src="<?= $podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-header" /> <img src="<?= $podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-header" />
<div class="flex px-4 py-2"> <div class="flex px-4 py-2">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>" <img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>"
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated" /> class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" />
<div class="flex flex-col"> <div class="flex flex-col">
<p class="font-semibold leading-none"><?= $podcast->title ?></p> <p class="font-semibold leading-none"><?= $podcast->title ?></p>
<p class="text-sm text-skin-muted">@<?= $podcast->handle ?></p> <p class="text-sm text-skin-muted">@<?= $podcast->handle ?></p>

View File

@ -55,7 +55,7 @@
return '<div class="flex">' . return '<div class="flex">' .
'<a href="' . '<a href="' .
route_to('person-view', $person->id) . route_to('person-view', $person->id) .
"\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" . "\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover aspect-square w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' . '<div class="flex flex-col ml-3">' .
$person->full_name . $person->full_name .
implode( implode(

View File

@ -9,16 +9,17 @@
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="flex flex-col gap-y-4">
<form action="<?= route_to('settings-instance') ?>" method="POST" class="flex flex-col gap-y-4" enctype="multipart/form-data"> <form action="<?= route_to('settings-instance') ?>" method="POST" enctype="multipart/form-data">
<?= csrf_field() ?> <?= csrf_field() ?>
<Forms.Section <Forms.Section
title="<?= lang('Settings.general.site_section_title') ?>"> title="<?= lang('Settings.instance.title') ?>">
<Forms.Field <Forms.Field
name="site_name" name="site_name"
label="<?= lang('Settings.general.site_name') ?>" label="<?= lang('Settings.instance.site_name') ?>"
value="<?= service('settings') value="<?= service('settings')
->get('App.siteName') ?>" ->get('App.siteName') ?>"
required="true" /> required="true" />
@ -26,7 +27,7 @@
<Forms.Field <Forms.Field
as="Textarea" as="Textarea"
name="site_description" name="site_description"
label="<?= lang('Settings.general.site_description') ?>" label="<?= lang('Settings.instance.site_description') ?>"
value="<?= service('settings') value="<?= service('settings')
->get('App.siteDescription') ?>" ->get('App.siteDescription') ?>"
required="true" required="true"
@ -36,24 +37,38 @@
<Forms.Field <Forms.Field
name="site_icon" name="site_icon"
type="file" type="file"
label="<?= lang('Settings.general.site_icon') ?>" label="<?= lang('Settings.instance.site_icon') ?>"
hint="<?= lang('Settings.general.site_icon_hint') ?>" hint="<?= lang('Settings.instance.site_icon_hint') ?>"
helper="<?= lang('Settings.general.site_icon_helper') ?>" helper="<?= lang('Settings.instance.site_icon_helper') ?>"
accept=".png,.jpeg,.jpg" accept=".png,.jpeg,.jpg"
class="flex-1" class="flex-1"
/> />
<?php if (config('App')->siteIcon['ico'] !== service('settings')->get('App.siteIcon')['ico']): ?> <?php if (config('App')->siteIcon['ico'] !== service('settings')->get('App.siteIcon')['ico']): ?>
<div class="relative ml-2"> <div class="relative ml-2">
<a href="<?= route_to('settings-instance-delete-icon') ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast -top-3 -right-3 focus:ring-accent" title="<?= lang('Settings.general.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin') ?></a> <a href="<?= route_to('settings-instance-delete-icon') ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast -top-3 -right-3 focus:ring-accent" title="<?= lang('Settings.instance.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin') ?></a>
<img src="<?= service('settings')->get('App.siteIcon')['64'] ?>" alt="<?= service('settings')->get('App.siteName') ?> Favicon" class="w-10 h-10" /> <img src="<?= service('settings')->get('App.siteIcon')['64'] ?>" alt="<?= service('settings')->get('App.siteName') ?> Favicon" class="w-10 h-10 aspect-square" />
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<Button variant="primary" type="submit" class="self-end"><?= lang('Settings.general.submit') ?></Button> <Button variant="primary" type="submit" class="self-end"><?= lang('Settings.instance.submit') ?></Button>
</Forms.Section> </Forms.Section>
</form> </form>
<form action="<?= route_to('settings-images-regenerate') ?>" method="POST" class="flex flex-col gap-y-4">
<?= csrf_field() ?>
<Forms.Section
title="<?= lang('Settings.images.title') ?>"
subtitle="<?= lang('Settings.images.subtitle') ?>" >
<Button variant="primary" type="submit" iconLeft="refresh"><?= lang('Settings.images.regenerate') ?></Button>
</Forms.Section>
</form>
</div>
<?= $this->endSection() ?> <?= $this->endSection() ?>

View File

@ -20,7 +20,7 @@
aria-expanded="false"><div class="relative mr-1"> aria-expanded="false"><div class="relative mr-1">
<?= icon('account-circle', 'text-3xl opacity-60') ?> <?= icon('account-circle', 'text-3xl opacity-60') ?>
<?= user() <?= user()
->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation" />' ?> ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" />' ?>
</div> </div>
<?= user()->username ?> <?= user()->username ?>
<?= icon('caret-down', 'ml-auto text-2xl') ?></button> <?= icon('caret-down', 'ml-auto text-2xl') ?></button>

View File

@ -17,7 +17,7 @@
<div class="flex flex-col items-start p-4 gap-y-4"> <div class="flex flex-col items-start p-4 gap-y-4">
<?php foreach ($persons as $person): ?> <?php foreach ($persons as $person): ?>
<div class="flex gap-x-2"> <div class="flex gap-x-2">
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-10 h-10 rounded-full" /> <img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-10 rounded-full aspect-square" />
<div class="flex flex-col"> <div class="flex flex-col">
<h4 class="text-sm font-semibold"> <h4 class="text-sm font-semibold">
<?php if ($person->information_url): ?> <?php if ($person->information_url): ?>

View File

@ -12,8 +12,6 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<link rel="canonical" href="<?= $episode->link ?>" />
<?= service('vite') <?= service('vite')
->asset('styles/index.css', 'css') ?> ->asset('styles/index.css', 'css') ?>
<?= service('vite') <?= service('vite')
@ -21,7 +19,7 @@
</head> </head>
<body class="flex" style="background: <?= $themeData['background'] ?>; color: <?= $themeData['text'] ?>;"> <body class="flex" style="background: <?= $themeData['background'] ?>; color: <?= $themeData['text'] ?>;">
<img src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->title ?>" class="flex-shrink w-36 h-36" /> <img src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->title ?>" class="flex-shrink w-36 h-36 aspect-square" />
<div class="flex flex-col items-start flex-1 min-w-0 px-4 pt-4 h-36"> <div class="flex flex-col items-start flex-1 min-w-0 px-4 pt-4 h-36">
<a href="https://castopod.org/" class="absolute top-0 right-0 mt-1 mr-2 text-2xl text-pine-500 hover:opacity-75" title="<?= lang('Common.powered_by', [ <a href="https://castopod.org/" class="absolute top-0 right-0 mt-1 mr-2 text-2xl text-pine-500 hover:opacity-75" title="<?= lang('Common.powered_by', [
'castopod' => 'Castopod', 'castopod' => 'Castopod',

View File

@ -10,7 +10,17 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('podcast-webmanifest', $podcast->handle) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>
@ -70,7 +80,7 @@
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= $episode->podcast->banner->small_url ?>');"></div> <div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= $episode->podcast->banner->small_url ?>');"></div>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div> <div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div>
<div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row"> <div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row">
<img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" loading="lazy" class="rounded-md shadow-xl h-36" /> <img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" loading="lazy" class="rounded-md shadow-xl h-36 aspect-square" />
<div class="flex flex-col items-start text-white"> <div class="flex flex-col items-start text-white">
<?= episode_numbering($episode->number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?> <?= episode_numbering($episode->number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?>
<h1 class="inline-flex items-baseline max-w-md mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2"><?= $episode->title ?></h1> <h1 class="inline-flex items-baseline max-w-md mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2"><?= $episode->title ?></h1>
@ -80,7 +90,7 @@
<div class="inline-flex flex-row-reverse"> <div class="inline-flex flex-row-reverse">
<?php $i = 0; ?> <?php $i = 0; ?>
<?php foreach ($episode->persons as $person): ?> <?php foreach ($episode->persons as $person): ?>
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-background-header last:ml-0" /> <img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full aspect-square border-background-header last:ml-0" />
<?php $i++; if ($i === 3) { <?php $i++; if ($i === 3) {
break; break;
}?> }?>

View File

@ -4,7 +4,7 @@
<?= format_duration($episode->audio_file_duration) ?> <?= format_duration($episode->audio_file_duration) ?>
</time> </time>
<img loading="lazy" src="<?= $episode->cover <img loading="lazy" src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 rounded-lg" /> ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 rounded-lg aspect-square" />
</div> </div>
<div class="flex items-center flex-1 gap-x-4"> <div class="flex items-center flex-1 gap-x-4">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">

View File

@ -1,5 +1,5 @@
<article class="relative z-10 flex w-full px-4 py-2 rounded-conditional-2xl gap-x-2"> <article class="relative z-10 flex w-full px-4 py-2 rounded-conditional-2xl gap-x-2">
<img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-10 h-10 rounded-full" /> <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex-1"> <div class="flex-1">
<header class="w-full mb-2 text-sm"> <header class="w-full mb-2 text-sm">
<a href="<?= $comment->actor <a href="<?= $comment->actor
@ -8,7 +8,7 @@
: 'target="_blank" rel="noopener noreferrer"' ?>> : 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $comment->actor <span class="mr-2 font-semibold truncate"><?= $comment->actor
->display_name ?></span> ->display_name ?></span>
<span class="text-sm text-skin-muted truncate">@<?= $comment->actor <span class="text-sm truncate text-skin-muted">@<?= $comment->actor
->username . ->username .
($comment->actor->is_local ($comment->actor->is_local
? '' ? ''

View File

@ -1,5 +1,5 @@
<article class="relative z-10 flex w-full p-4 shadow bg-elevated rounded-conditional-2xl gap-x-2"> <article class="relative z-10 flex w-full p-4 shadow bg-elevated rounded-conditional-2xl gap-x-2">
<img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-10 h-10 rounded-full" /> <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex-1"> <div class="flex-1">
<header class="w-full mb-2 text-sm"> <header class="w-full mb-2 text-sm">
<a href="<?= $comment->actor->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local <a href="<?= $comment->actor->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local

View File

@ -1,6 +1,6 @@
<article class="flex px-6 py-4 bg-base gap-x-2"> <article class="flex px-6 py-4 bg-base gap-x-2">
<img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor <img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor
->display_name ?>" class="z-10 w-10 h-10 rounded-full ring-gray-50 ring-2" /> ->display_name ?>" class="z-10 w-10 h-10 rounded-full ring-gray-50 ring-2 aspect-square" />
<div class="flex flex-col flex-1 min-w-0"> <div class="flex flex-col flex-1 min-w-0">
<header class="flex items-center mb-2"> <header class="flex items-center mb-2">
<a href="<?= $reply->actor <a href="<?= $reply->actor

View File

@ -15,7 +15,7 @@ if ($comment->in_reply_to_id): ?>
<form action="<?= route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id) ?>" method="POST" class="flex px-6 pt-8 pb-4 gap-x-2 bg-base"> <form action="<?= route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id) ?>" method="POST" class="flex px-6 pt-8 pb-4 gap-x-2 bg-base">
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-10 h-10 rounded-full" /> ->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<Forms.Textarea <Forms.Textarea
name="message" name="message"

View File

@ -18,5 +18,5 @@ $navigationItems = [
<?php $isActive = url_is($item['uri']); ?> <?php $isActive = url_is($item['uri']); ?>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-accent-base border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?><span class="px-2 ml-1 font-semibold rounded-full bg-base"><?= $item['labelInfo'] ?></span></a> <a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-accent-base border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?><span class="px-2 ml-1 font-semibold rounded-full bg-base"><?= $item['labelInfo'] ?></span></a>
<?php endforeach; ?> <?php endforeach; ?>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler"><?= icon('menu') ?></button> <button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu') ?></button>
</nav> </nav>

View File

@ -5,7 +5,7 @@
</time> </time>
<img <img
src="<?= $episode->cover->thumbnail_url ?>" src="<?= $episode->cover->thumbnail_url ?>"
alt="<?= $episode->title ?>" class="w-24 h-24"/> alt="<?= $episode->title ?>" class="w-24 h-24 aspect-square"/>
</div> </div>
<div class="flex flex-col flex-1 px-4 py-2"> <div class="flex flex-col flex-1 px-4 py-2">
<div class="inline-flex"> <div class="inline-flex">

View File

@ -9,7 +9,7 @@
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-10 h-10 rounded-full" /> ->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2"> <div class="flex flex-col flex-1 min-w-0 gap-y-2">
<input name="episode_url" value="<?= $episode->link ?>" type="hidden" /> <input name="episode_url" value="<?= $episode->link ?>" type="hidden" />
<Forms.Textarea <Forms.Textarea

View File

@ -9,7 +9,7 @@
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-10 h-10 rounded-full" /> ->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2"> <div class="flex flex-col flex-1 min-w-0 gap-y-2">
<Forms.Textarea <Forms.Textarea
name="message" name="message"

View File

@ -14,6 +14,16 @@
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>
@ -50,7 +60,7 @@
<article class="text-white"> <article class="text-white">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div> <div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden"> <div class="w-full h-full overflow-hidden">
<img alt="<?= $podcast->title ?>" src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" /> <img alt="<?= $podcast->title ?>" src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" />
</div> </div>
<div class="absolute bottom-0 left-0 z-20 px-4 pb-2"> <div class="absolute bottom-0 left-0 z-20 px-4 pb-2">
<h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2> <h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2>

View File

@ -10,6 +10,16 @@
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>

View File

@ -17,7 +17,7 @@
<div class="flex mt-2 mb-2"> <div class="flex mt-2 mb-2">
<img src="<?= $persons['thumbnail_url'] ?>" alt="<?= $persons[ <img src="<?= $persons['thumbnail_url'] ?>" alt="<?= $persons[
'full_name' 'full_name'
] ?>" class="object-cover w-16 h-16 rounded-full md:h-24 md:w-24 border-gray" /> ] ?>" class="object-cover w-16 rounded-full aspect-square md:h-24 md:w-24 border-gray" />
<div class="flex flex-col ml-3 mr-4"> <div class="flex flex-col ml-3 mr-4">
<span class="text-lg font-semibold text-skin-muted md:text-xl"> <span class="text-lg font-semibold text-skin-muted md:text-xl">
<?= $persons['full_name'] ?> <?= $persons['full_name'] ?>

View File

@ -15,6 +15,16 @@
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= service('vite') <?= service('vite')
->asset('styles/index.css', 'css') ?> ->asset('styles/index.css', 'css') ?>

View File

@ -10,7 +10,17 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('podcast-webmanifest', $podcast->handle) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>
@ -35,7 +45,7 @@
<header class="relative z-50 flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= $podcast->banner->medium_url ?>');"> <header class="relative z-50 flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= $podcast->banner->medium_url ?>');">
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient"></div> <div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient"></div>
<div class="z-10 flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4"> <div class="z-10 flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated" /> <img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated aspect-square" />
<div class="relative flex flex-col text-white -top-2 sm:top-0 md:top-2"> <div class="relative flex flex-col text-white -top-2 sm:top-0 md:top-2">
<h1 class="text-lg font-bold leading-none line-clamp-2 md:leading-none md:text-2xl font-display"><?= $podcast->title ?><span class="ml-1 font-sans text-base font-normal">@<?= $podcast->handle ?></span></h1> <h1 class="text-lg font-bold leading-none line-clamp-2 md:leading-none md:text-2xl font-display"><?= $podcast->title ?><span class="ml-1 font-sans text-base font-normal">@<?= $podcast->handle ?></span></h1>
<span class="text-xs"><?= lang('Podcast.followers', [ <span class="text-xs"><?= lang('Podcast.followers', [

View File

@ -20,5 +20,5 @@ $navigationItems = [
<?php $isActive = url_is($item['uri']); ?> <?php $isActive = url_is($item['uri']); ?>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-accent-base border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?></a> <a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-accent-base border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?></a>
<?php endforeach; ?> <?php endforeach; ?>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler"><?= icon('menu') ?></button> <button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu') ?></button>
</nav> </nav>

View File

@ -25,7 +25,7 @@
<div class="inline-flex flex-row-reverse"> <div class="inline-flex flex-row-reverse">
<?php $i = 0; ?> <?php $i = 0; ?>
<?php foreach ($podcast->persons as $person): ?> <?php foreach ($podcast->persons as $person): ?>
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-background-base last:ml-0" /> <img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 -ml-5 border-2 rounded-full aspect-square border-background-base last:ml-0" />
<?php $i++; if ($i === 3) { <?php $i++; if ($i === 3) {
break; break;
}?> }?>

View File

@ -10,7 +10,7 @@
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-10 h-10 rounded-full" /> ->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2"> <div class="flex flex-col flex-1 min-w-0 gap-y-2">
<Forms.Textarea <Forms.Textarea
name="message" name="message"

View File

@ -10,8 +10,18 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('podcast-webmanifest', $actor->podcast->handle) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>
<?= service('vite') <?= service('vite')
@ -27,10 +37,10 @@
'Fediverse.follow.subtitle', 'Fediverse.follow.subtitle',
) ?></h1> ) ?></h1>
<div class="flex flex-col w-full max-w-xs -mt-24 overflow-hidden shadow bg-elevated rounded-xl"> <div class="flex flex-col w-full max-w-xs -mt-24 overflow-hidden shadow bg-elevated rounded-xl">
<img src="<?= $actor->podcast->banner->small_url ?>" alt="" class="object-cover w-full h-32 bg-header" /> <img src="<?= $actor->podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-header" />
<div class="flex px-4 py-2"> <div class="flex px-4 py-2">
<img src="<?= $actor->avatar_image_url ?>" alt="<?= $actor->display_name ?>" <img src="<?= $actor->avatar_image_url ?>" alt="<?= $actor->display_name ?>"
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated" /> class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" />
<div class="flex flex-col"> <div class="flex flex-col">
<p class="font-semibold"><?= $actor->display_name ?></p> <p class="font-semibold"><?= $actor->display_name ?></p>
<p class="text-sm text-skin-muted">@<?= $actor->username ?></p> <p class="text-sm text-skin-muted">@<?= $actor->username ?></p>

View File

@ -1,7 +1,7 @@
<article class="relative z-10 w-full shadow bg-elevated sm:rounded-conditional-2xl"> <article class="relative z-10 w-full shadow bg-elevated sm:rounded-conditional-2xl">
<header class="flex px-6 py-4 gap-x-2"> <header class="flex px-6 py-4 gap-x-2">
<img src="<?= $post->actor <img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-10 h-10 rounded-full" /> ->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<a href="<?= $post->actor <a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post ->uri ?>" class="flex items-baseline hover:underline" <?= $post

View File

@ -20,7 +20,7 @@ if ($post->in_reply_to_id): ?>
<form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $post->id) ?>" method="POST" class="flex gap-x-2" > <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $post->id) ?>" method="POST" class="flex gap-x-2" >
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-10 h-10 rounded-full" /> ->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<Forms.Textarea <Forms.Textarea
name="message" name="message"

View File

@ -8,7 +8,7 @@ if ($preview_card->type === 'image'): ?>
'external-link', 'external-link',
'absolute inset-0 m-auto text-6xl bg-accent-base bg-opacity-50 group-hover:bg-opacity-100 text-accent-contrast rounded-full p-2', 'absolute inset-0 m-auto text-6xl bg-accent-base bg-opacity-50 group-hover:bg-opacity-100 text-accent-contrast rounded-full p-2',
) ?> ) ?>
<img src="<?= $preview_card->image ?>" alt="<?= $preview_card->title ?>" class="object-cover w-full h-80" /> <img src="<?= $preview_card->image ?>" alt="<?= $preview_card->title ?>" class="object-cover w-full aspect-video" />
</div> </div>
<?php endif; ?> <?php endif; ?>
@ -36,7 +36,7 @@ if ($preview_card->type === 'image'): ?>
<?php else: ?> <?php else: ?>
<a href="<?= $preview_card->url ?>" class="flex items-center bg-highlight"> <a href="<?= $preview_card->url ?>" class="flex items-center bg-highlight">
<?php if ($preview_card->image): ?> <?php if ($preview_card->image): ?>
<img src="<?= $preview_card->image ?>" alt="<?= $preview_card->title ?>" class="object-cover w-20 h-20" /> <img src="<?= $preview_card->image ?>" alt="<?= $preview_card->title ?>" class="object-cover w-20 aspect-square" />
<?php endif; ?> <?php endif; ?>
<p class="flex flex-col flex-1 px-4 py-2"> <p class="flex flex-col flex-1 px-4 py-2">
<span class="text-xs tracking-wider uppercase text-skin-muted"><?= $preview_card->provider_name ?></span> <span class="text-xs tracking-wider uppercase text-skin-muted"><?= $preview_card->provider_name ?></span>

View File

@ -8,7 +8,7 @@
]) ?></p> ]) ?></p>
<header class="flex px-6 py-4 gap-x-2"> <header class="flex px-6 py-4 gap-x-2">
<img src="<?= $post->actor <img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-10 h-10 rounded-full" /> ->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-10 h-10 rounded-full aspect-square" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<a href="<?= $post->actor <a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post ->uri ?>" class="flex items-baseline hover:underline" <?= $post

View File

@ -1,6 +1,6 @@
<article class="flex px-6 py-4 bg-base gap-x-2"> <article class="flex px-6 py-4 bg-base gap-x-2">
<img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor <img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor
->display_name ?>" class="z-10 w-10 h-10 rounded-full ring-background-base ring-2" /> ->display_name ?>" class="z-10 w-10 h-10 rounded-full ring-background-base ring-2 aspect-square" />
<div class="flex flex-col flex-1 min-w-0"> <div class="flex flex-col flex-1 min-w-0">
<header class="flex items-center mb-2"> <header class="flex items-center mb-2">
<a href="<?= $reply->actor <a href="<?= $reply->actor

View File

@ -8,7 +8,17 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>"> <link rel="manifest" href="<?= route_to('podcast-webmanifest', $post->actor->podcast->handle) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
<?= $metatags ?> <?= $metatags ?>

View File

@ -11,7 +11,6 @@
<link rel="icon" type="image/x-icon" href="<?= service('settings') <link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" /> ->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>"> <link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<?= service('vite') <?= service('vite')
->asset('styles/index.css', 'css') ?> ->asset('styles/index.css', 'css') ?>
<?= service('vite') <?= service('vite')

View File

@ -1,4 +1,5 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { ManifestCSS } from "./vite-manifest-css"; import { ManifestCSS } from "./vite-manifest-css";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@ -24,5 +25,11 @@ export default defineConfig({
}, },
}, },
}, },
plugins: [ManifestCSS()], plugins: [
ManifestCSS(),
VitePWA({
manifest: false,
outDir: "../../public",
}),
],
}); });