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
@ -37,6 +37,7 @@ RUN apt-get update \
|
||||
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
|
||||
libicu-dev \
|
||||
libpng-dev \
|
||||
libwebp-dev \
|
||||
libjpeg-dev \
|
||||
zlib1g-dev \
|
||||
libzip-dev \
|
||||
@ -44,7 +45,7 @@ RUN apt-get update \
|
||||
&& docker-php-ext-install intl \
|
||||
&& docker-php-ext-install zip \
|
||||
# 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 \
|
||||
# redis extension for cache
|
||||
&& pecl install -o -f redis \
|
||||
|
@ -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)
|
||||
- [libcurl](https://php.net/manual/en/curl.requirements.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:
|
||||
|
||||
|
@ -47,15 +47,57 @@ class Images extends BaseConfig
|
||||
*
|
||||
* Array values are as follows: 'name' => [width, height]
|
||||
*
|
||||
* @var array<string, int[]>
|
||||
* @var array<string, array<string, int|string>>
|
||||
*/
|
||||
public array $podcastCoverSizes = [
|
||||
'tiny' => [40, 40],
|
||||
'thumbnail' => [150, 150],
|
||||
'medium' => [320, 320],
|
||||
'large' => [1024, 1024],
|
||||
'feed' => [1400, 1400],
|
||||
'id3' => [500, 500],
|
||||
'tiny' => [
|
||||
'width' => 40,
|
||||
'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',
|
||||
],
|
||||
'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
|
||||
*
|
||||
* Array values are as follows: 'name' => [width, height]
|
||||
*
|
||||
* @var array<string, int[]>
|
||||
* @var array<string, array<string, int|string>>
|
||||
*/
|
||||
public array $podcastBannerSizes = [
|
||||
'small' => [320, 128],
|
||||
'medium' => [960, 320],
|
||||
'large' => [1500, 500],
|
||||
'small' => [
|
||||
'width' => 320,
|
||||
'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';
|
||||
@ -84,11 +137,27 @@ class Images extends BaseConfig
|
||||
*
|
||||
* Array values are as follows: 'name' => [width, height]
|
||||
*
|
||||
* @var array<string, int[]>
|
||||
* @var array<string, array<string, int|string>>
|
||||
*/
|
||||
public array $personAvatarSizes = [
|
||||
'tiny' => [40, 40],
|
||||
'thumbnail' => [150, 150],
|
||||
'medium' => [320, 320],
|
||||
'tiny' => [
|
||||
'width' => 40,
|
||||
'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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -65,6 +65,10 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
|
||||
$routes->get('/', 'PodcastController::activity/$1', [
|
||||
'as' => 'podcast-activity',
|
||||
]);
|
||||
$routes->get('manifest.webmanifest', 'WebmanifestController::podcastManifest/$1', [
|
||||
'as' => 'podcast-webmanifest',
|
||||
]);
|
||||
|
||||
// override default Fediverse Library's actor route
|
||||
$routes->options('/', 'ActivityPubController::preflight');
|
||||
$routes->get('/', 'PodcastController::activity/$1', [
|
||||
|
@ -204,9 +204,9 @@ class EpisodeController extends BaseController
|
||||
'height' => 144,
|
||||
'thumbnail_url' => $this->episode->cover->large_url,
|
||||
'thumbnail_width' => config('Images')
|
||||
->podcastCoverSizes['large'][0],
|
||||
->podcastCoverSizes['large']['width'],
|
||||
'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_url', $this->podcast->link);
|
||||
$oembed->addChild('thumbnail', $this->episode->cover->large_url);
|
||||
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['large'][0]);
|
||||
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['large'][1]);
|
||||
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['large']['width']);
|
||||
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['large']['height']);
|
||||
$oembed->addChild(
|
||||
'html',
|
||||
htmlentities(
|
||||
|
@ -10,11 +10,44 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
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
|
||||
{
|
||||
$webmanifest = [
|
||||
@ -22,8 +55,13 @@ class WebmanifestController extends Controller
|
||||
->get('App.siteName'),
|
||||
'description' => service('settings')
|
||||
->get('App.siteDescription'),
|
||||
'lang' => service('request')
|
||||
->getLocale(),
|
||||
'start_url' => base_url(),
|
||||
'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' => [
|
||||
[
|
||||
'src' => service('settings')
|
||||
@ -42,4 +80,39 @@ class WebmanifestController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -41,4 +41,22 @@ class Actor extends FediverseActor
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,9 @@ class Episode extends Entity
|
||||
public function getCover(): Image
|
||||
{
|
||||
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()
|
||||
|
@ -28,7 +28,7 @@ class Image extends Entity
|
||||
{
|
||||
protected Images $config;
|
||||
|
||||
protected ?File $file = null;
|
||||
protected File $file;
|
||||
|
||||
protected string $dirname;
|
||||
|
||||
@ -38,7 +38,16 @@ class Image extends Entity
|
||||
|
||||
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 === '') {
|
||||
throw new RuntimeException('File or path must be set to create an Image.');
|
||||
@ -63,11 +72,17 @@ class Image extends Entity
|
||||
] = pathinfo($path);
|
||||
}
|
||||
|
||||
if ($file === null) {
|
||||
helper('media');
|
||||
$file = new File(media_path($path));
|
||||
}
|
||||
|
||||
$this->file = $file;
|
||||
$this->dirname = $dirname;
|
||||
$this->filename = $filename;
|
||||
$this->extension = $extension;
|
||||
$this->mimetype = $mimetype;
|
||||
$this->sizes = $sizes;
|
||||
}
|
||||
|
||||
public function __get($property)
|
||||
@ -91,7 +106,24 @@ class Image extends Entity
|
||||
if ($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')) {
|
||||
helper('media');
|
||||
@ -111,15 +143,11 @@ class Image extends Entity
|
||||
|
||||
public function getFile(): File
|
||||
{
|
||||
if ($this->file === null) {
|
||||
$this->file = new File($this->path);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@ -127,6 +155,7 @@ class Image extends Entity
|
||||
|
||||
$this->dirname = $dirname;
|
||||
$this->filename = $filename;
|
||||
$this->sizes = $sizes;
|
||||
|
||||
save_media($this->file, $this->dirname, $this->filename);
|
||||
|
||||
@ -136,8 +165,8 @@ class Image extends Entity
|
||||
$pathProperty = $name . '_path';
|
||||
$imageService
|
||||
->withFile(media_path($this->path))
|
||||
->resize($size[0], $size[1])
|
||||
->save(media_path($this->{$pathProperty}));
|
||||
->resize($size['width'], $size['height']);
|
||||
$imageService->save(media_path($this->{$pathProperty}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,10 +77,12 @@ class Person extends Entity
|
||||
public function getAvatar(): Image
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,7 +211,7 @@ class Podcast extends Entity
|
||||
|
||||
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')
|
||||
->podcastBannerDefaultPath,
|
||||
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
|
||||
|
@ -24,7 +24,7 @@ if (! function_exists('get_podcast_metatags')) {
|
||||
$schema = new Schema(
|
||||
new Thing('PodcastSeries', [
|
||||
'name' => $podcast->title,
|
||||
'url' => url_to('podcast-activity', $podcast->handle),
|
||||
'url' => $podcast->link,
|
||||
'image' => $podcast->cover->feed_url,
|
||||
'description' => $podcast->description,
|
||||
'webFeed' => $podcast->feed_url,
|
||||
@ -41,8 +41,8 @@ if (! function_exists('get_podcast_metatags')) {
|
||||
->description(htmlspecialchars($podcast->description))
|
||||
->image((string) $podcast->cover->large_url)
|
||||
->canonical((string) current_url())
|
||||
->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0])
|
||||
->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1])
|
||||
->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
|
||||
->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
|
||||
->og('locale', $podcast->language_code)
|
||||
->og('site_name', service('settings')->get('App.siteName'));
|
||||
|
||||
@ -70,7 +70,7 @@ if (! function_exists('get_episode_metatags')) {
|
||||
]),
|
||||
'partOfSeries' => new Thing('PodcastSeries', [
|
||||
'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')
|
||||
->canonical($episode->link)
|
||||
->og('site_name', service('settings')->get('App.siteName'))
|
||||
->og('image:width', (string) config('Images')->podcastCoverSizes['large'][0])
|
||||
->og('image:height', (string) config('Images')->podcastCoverSizes['large'][1])
|
||||
->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
|
||||
->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
|
||||
->og('locale', $episode->podcast->language_code)
|
||||
->og('audio', $episode->audio_file_opengraph_url)
|
||||
->og('audio:type', $episode->audio_file_mimetype)
|
||||
|
@ -485,9 +485,9 @@ class PodcastModel extends Model
|
||||
// update values
|
||||
$actor->display_name = $podcast->title;
|
||||
$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->cover_image_url = $podcast->banner->large_url;
|
||||
$actor->cover_image_url = $podcast->banner->federation_url;
|
||||
$actor->cover_image_mimetype = $podcast->banner->mimetype;
|
||||
|
||||
if ($actor->hasChanged()) {
|
||||
|
6
app/Resources/icons/refresh.svg
Normal 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
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c0a25c3d11c806b4bc62eafb22902bc8",
|
||||
"content-hash": "afb6585b90ed08cc8a257f346ab1c416",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -603,8 +603,8 @@
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.0",
|
||||
"phpunit/phpunit": "^4.8 || ^5.0 || ^6.1 || ^7.5 || ^8.5"
|
||||
"jakub-onderka/php-parallel-lint": "^0.9 || ^1.0",
|
||||
"phpunit/phpunit": "^4.8|^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-SimpleXML": "SimpleXML extension is required to analyze RIFF/WAV/BWF audio files (also requires `ext-libxml`).",
|
||||
|
@ -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
|
||||
> background:
|
||||
>
|
||||
> - `castopod-host_app`: a php based container with CodeIgniter4 requirements
|
||||
> installed
|
||||
> - `castopod-host_app`: a php based container with Castopod Host
|
||||
> requirements installed
|
||||
> - `castopod-host_redis`: a [redis](https://redis.io/) database to handle
|
||||
> queries and pages caching
|
||||
> - `castopod-host_mariadb`: a [mariadb](https://mariadb.org/) server for
|
||||
|
@ -31,6 +31,10 @@ $routes->group(
|
||||
'as' => 'settings-instance-delete-icon',
|
||||
'filter' => 'permission:settings-manage',
|
||||
]);
|
||||
$routes->post('instance-images-regenerate', 'SettingsController::regenerateImages', [
|
||||
'as' => 'settings-images-regenerate',
|
||||
'filter' => 'permission:settings-manage',
|
||||
]);
|
||||
$routes->get('theme', 'SettingsController::theme', [
|
||||
'as' => 'settings-theme',
|
||||
'filter' => 'permission:settings-manage',
|
||||
|
@ -10,6 +10,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Models\PersonModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use PHP_ICO;
|
||||
|
||||
@ -75,20 +77,20 @@ class SettingsController extends BaseController
|
||||
service('image')
|
||||
->withFile(ROOTPATH . 'public/media/site/icon.png')
|
||||
->resize($size, $size)
|
||||
->save(ROOTPATH . "public/media/site/icon-{$size}.{$randomHash}.png");
|
||||
->save(media_path("/site/icon-{$size}.{$randomHash}.png"));
|
||||
}
|
||||
|
||||
service('settings')
|
||||
->set('App.siteIcon', [
|
||||
'ico' => "/media/site/favicon.{$randomHash}.ico",
|
||||
'64' => "/media/site/icon-64.{$randomHash}.png",
|
||||
'180' => "/media/site/icon-180.{$randomHash}.png",
|
||||
'192' => "/media/site/icon-192.{$randomHash}.png",
|
||||
'512' => "/media/site/icon-512.{$randomHash}.png",
|
||||
'ico' => media_path("/site/favicon.{$randomHash}.ico"),
|
||||
'64' => media_path("/site/icon-64.{$randomHash}.png"),
|
||||
'180' => media_path("/site/icon-180.{$randomHash}.png"),
|
||||
'192' => media_path("/site/icon-192.{$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
|
||||
@ -100,7 +102,52 @@ class SettingsController extends BaseController
|
||||
service('settings')
|
||||
->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
|
||||
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'toggle_sidebar' => 'Toggle sidebar',
|
||||
'go_to_website' => 'View site',
|
||||
'go_to_admin' => 'Go to admin',
|
||||
'dashboard' => 'Dashboard',
|
||||
|
@ -10,8 +10,8 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'title' => 'General settings',
|
||||
'general' => [
|
||||
'site_section_title' => 'Instance',
|
||||
'instance' => [
|
||||
'title' => 'Instance',
|
||||
'site_icon' => '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.',
|
||||
@ -19,9 +19,15 @@ return [
|
||||
'site_name' => 'Site name',
|
||||
'site_description' => 'Site description',
|
||||
'submit' => 'Save',
|
||||
'instanceEditSuccess' => 'Instance has been updated successfully!',
|
||||
'editSuccess' => 'Instance has been updated 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' => [
|
||||
'title' => 'Theme',
|
||||
'accent_section_title' => 'Accent color',
|
||||
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'toggle_sidebar' => 'Afficher ou cacher la barre latérale',
|
||||
'go_to_website' => 'Visiter le site',
|
||||
'dashboard' => 'Tableau de bord',
|
||||
'admin' => 'Accueil',
|
||||
|
@ -10,8 +10,8 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'title' => 'Paramètres généraux',
|
||||
'general' => [
|
||||
'site_section_title' => 'Instance',
|
||||
'instance' => [
|
||||
'title' => 'Instance',
|
||||
'site_icon' => '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.',
|
||||
@ -19,9 +19,15 @@ return [
|
||||
'site_name' => 'Titre du site',
|
||||
'site_description' => 'Description du site',
|
||||
'submit' => 'Sauvegarder',
|
||||
'instanceEditSuccess' => 'L’instance a bien été mise à jour !',
|
||||
'editSuccess' => 'L’instance a bien été mise à jour !',
|
||||
'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 à l’origine.',
|
||||
'regenerate' => 'Regénérer les images',
|
||||
'regenerationSuccess' => 'Toutes les images ont été regénérés avec succès !',
|
||||
],
|
||||
'theme' => [
|
||||
'title' => 'Thème',
|
||||
'accent_section_title' => 'Couleur d’accentuation',
|
||||
@ -32,6 +38,7 @@ return [
|
||||
'lake' => 'Lac',
|
||||
'jacaranda' => 'Jacaranda',
|
||||
'onyx' => 'Onyx',
|
||||
'submit' => 'Sauvegarder',
|
||||
'setInstanceThemeSuccess' => 'Le thème a bien été mis à jour !',
|
||||
],
|
||||
];
|
||||
|
4974
package-lock.json
generated
@ -83,7 +83,12 @@
|
||||
"svgo": "^2.8.0",
|
||||
"tailwindcss": "^3.0.0-alpha.1",
|
||||
"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": {
|
||||
"*.{js,ts,css,md,json}": "prettier --write",
|
||||
|
Before Width: | Height: | Size: 22 KiB |
BIN
public/media/castopod-avatar-default_medium.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
public/media/castopod-avatar-default_thumbnail.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
public/media/castopod-avatar-default_tiny.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 6.8 KiB |
BIN
public/media/castopod-banner-default_medium.webp
Normal file
After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/media/castopod-banner-default_small.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -52,6 +52,7 @@ module.exports = {
|
||||
subtle: withOpacity("--color-border-subtle"),
|
||||
contrast: withOpacity("--color-border-contrast"),
|
||||
navigation: withOpacity("--color-border-navigation"),
|
||||
"navigation-bg": withOpacity("--color-background-navigation"),
|
||||
accent: {
|
||||
base: withOpacity("--color-accent-base"),
|
||||
hover: withOpacity("--color-accent-hover"),
|
||||
|
@ -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">
|
||||
<button type="button"
|
||||
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">
|
||||
<a href="<?= route_to(
|
||||
'admin',
|
||||
@ -25,7 +25,7 @@
|
||||
aria-expanded="false"><div class="relative mr-1">
|
||||
<?= icon('account-circle', 'text-3xl opacity-60') ?>
|
||||
<?= 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>
|
||||
<?= user()->username ?>
|
||||
<?= icon('caret-down', 'ml-auto text-2xl') ?></button>
|
||||
|
@ -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">
|
||||
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
|
||||
<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>
|
||||
<?= 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">
|
||||
|
@ -12,7 +12,7 @@ $podcastNavigation = [
|
||||
<img
|
||||
src="<?= $podcast->cover->tiny_url ?>"
|
||||
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>
|
||||
</a>
|
||||
@ -20,7 +20,7 @@ $podcastNavigation = [
|
||||
<img
|
||||
src="<?= $episode->cover->thumbnail_url ?>"
|
||||
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">
|
||||
<span class="w-full font-semibold truncate" title="<?= $episode->title ?>"><?= $episode->title ?></span>
|
||||
|
@ -34,7 +34,7 @@
|
||||
$episode->audio_file_duration,
|
||||
) .
|
||||
'</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>' .
|
||||
'<a class="overflow-x-hidden text-sm hover:underline" href="' . route_to(
|
||||
'episode-view',
|
||||
|
@ -58,7 +58,7 @@
|
||||
return '<div class="flex">' .
|
||||
'<a href="' .
|
||||
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">' .
|
||||
$person->full_name .
|
||||
implode(
|
||||
|
@ -28,7 +28,7 @@
|
||||
<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="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">
|
||||
<p class="flex items-baseline min-w-0">
|
||||
<span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span>
|
||||
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="flex border-y">
|
||||
<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">
|
||||
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">
|
||||
<div class="flex items-baseline">
|
||||
|
@ -29,7 +29,7 @@
|
||||
<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="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">
|
||||
<p class="flex items-baseline min-w-0">
|
||||
<span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span>
|
||||
@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="flex border-y">
|
||||
<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">
|
||||
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">
|
||||
<div class="flex items-baseline">
|
||||
|
@ -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">
|
||||
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
|
||||
<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 class="absolute z-20">
|
||||
<h2 class="px-4 py-2 font-semibold leading-tight"><?= $person->full_name ?></h2>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<img
|
||||
src="<?= $person->avatar->medium_url ?>"
|
||||
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">
|
||||
<?= $person->full_name ?>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<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" />
|
||||
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 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>
|
||||
|
@ -39,7 +39,7 @@ $podcastNavigation = [
|
||||
<img
|
||||
src="<?= $podcast->cover->thumbnail_url ?>"
|
||||
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">
|
||||
<span class="w-full font-semibold truncate" title="<?= $podcast->title ?>"><?= $podcast->title ?></span>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<img src="<?= $podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-header" />
|
||||
<div class="flex px-4 py-2">
|
||||
<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">
|
||||
<p class="font-semibold leading-none"><?= $podcast->title ?></p>
|
||||
<p class="text-sm text-skin-muted">@<?= $podcast->handle ?></p>
|
||||
|
@ -55,7 +55,7 @@
|
||||
return '<div class="flex">' .
|
||||
'<a href="' .
|
||||
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">' .
|
||||
$person->full_name .
|
||||
implode(
|
||||
|
@ -9,16 +9,17 @@
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $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() ?>
|
||||
|
||||
<Forms.Section
|
||||
title="<?= lang('Settings.general.site_section_title') ?>">
|
||||
title="<?= lang('Settings.instance.title') ?>">
|
||||
|
||||
<Forms.Field
|
||||
name="site_name"
|
||||
label="<?= lang('Settings.general.site_name') ?>"
|
||||
label="<?= lang('Settings.instance.site_name') ?>"
|
||||
value="<?= service('settings')
|
||||
->get('App.siteName') ?>"
|
||||
required="true" />
|
||||
@ -26,7 +27,7 @@
|
||||
<Forms.Field
|
||||
as="Textarea"
|
||||
name="site_description"
|
||||
label="<?= lang('Settings.general.site_description') ?>"
|
||||
label="<?= lang('Settings.instance.site_description') ?>"
|
||||
value="<?= service('settings')
|
||||
->get('App.siteDescription') ?>"
|
||||
required="true"
|
||||
@ -36,24 +37,38 @@
|
||||
<Forms.Field
|
||||
name="site_icon"
|
||||
type="file"
|
||||
label="<?= lang('Settings.general.site_icon') ?>"
|
||||
hint="<?= lang('Settings.general.site_icon_hint') ?>"
|
||||
helper="<?= lang('Settings.general.site_icon_helper') ?>"
|
||||
label="<?= lang('Settings.instance.site_icon') ?>"
|
||||
hint="<?= lang('Settings.instance.site_icon_hint') ?>"
|
||||
helper="<?= lang('Settings.instance.site_icon_helper') ?>"
|
||||
accept=".png,.jpeg,.jpg"
|
||||
class="flex-1"
|
||||
/>
|
||||
<?php if (config('App')->siteIcon['ico'] !== service('settings')->get('App.siteIcon')['ico']): ?>
|
||||
<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>
|
||||
<img src="<?= service('settings')->get('App.siteIcon')['64'] ?>" alt="<?= service('settings')->get('App.siteName') ?> Favicon" class="w-10 h-10" />
|
||||
<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 aspect-square" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
|
||||
</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() ?>
|
||||
|
@ -20,7 +20,7 @@
|
||||
aria-expanded="false"><div class="relative mr-1">
|
||||
<?= icon('account-circle', 'text-3xl opacity-60') ?>
|
||||
<?= 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>
|
||||
<?= user()->username ?>
|
||||
<?= icon('caret-down', 'ml-auto text-2xl') ?></button>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div class="flex flex-col items-start p-4 gap-y-4">
|
||||
<?php foreach ($persons as $person): ?>
|
||||
<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">
|
||||
<h4 class="text-sm font-semibold">
|
||||
<?php if ($person->information_url): ?>
|
||||
|
@ -12,8 +12,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<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')
|
||||
->asset('styles/index.css', 'css') ?>
|
||||
<?= service('vite')
|
||||
@ -21,7 +19,7 @@
|
||||
</head>
|
||||
|
||||
<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">
|
||||
<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',
|
||||
|
@ -10,7 +10,17 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<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 ?>
|
||||
|
||||
@ -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-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">
|
||||
<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">
|
||||
<?= 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>
|
||||
@ -80,7 +90,7 @@
|
||||
<div class="inline-flex flex-row-reverse">
|
||||
<?php $i = 0; ?>
|
||||
<?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) {
|
||||
break;
|
||||
}?>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<?= format_duration($episode->audio_file_duration) ?>
|
||||
</time>
|
||||
<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 class="flex items-center flex-1 gap-x-4">
|
||||
<div class="flex flex-col flex-1">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<header class="w-full mb-2 text-sm">
|
||||
<a href="<?= $comment->actor
|
||||
@ -8,7 +8,7 @@
|
||||
: 'target="_blank" rel="noopener noreferrer"' ?>>
|
||||
<span class="mr-2 font-semibold truncate"><?= $comment->actor
|
||||
->display_name ?></span>
|
||||
<span class="text-sm text-skin-muted truncate">@<?= $comment->actor
|
||||
<span class="text-sm truncate text-skin-muted">@<?= $comment->actor
|
||||
->username .
|
||||
($comment->actor->is_local
|
||||
? ''
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<header class="w-full mb-2 text-sm">
|
||||
<a href="<?= $comment->actor->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local
|
||||
|
@ -1,6 +1,6 @@
|
||||
<article class="flex px-6 py-4 bg-base gap-x-2">
|
||||
<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">
|
||||
<header class="flex items-center mb-2">
|
||||
<a href="<?= $reply->actor
|
||||
|
@ -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">
|
||||
<img src="<?= 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">
|
||||
<Forms.Textarea
|
||||
name="message"
|
||||
|
@ -18,5 +18,5 @@ $navigationItems = [
|
||||
<?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>
|
||||
<?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>
|
@ -5,7 +5,7 @@
|
||||
</time>
|
||||
<img
|
||||
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 class="flex flex-col flex-1 px-4 py-2">
|
||||
<div class="inline-flex">
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<img src="<?= 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">
|
||||
<input name="episode_url" value="<?= $episode->link ?>" type="hidden" />
|
||||
<Forms.Textarea
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<img src="<?= 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">
|
||||
<Forms.Textarea
|
||||
name="message"
|
||||
|
@ -14,6 +14,16 @@
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
|
||||
<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 ?>
|
||||
|
||||
@ -50,7 +60,7 @@
|
||||
<article class="text-white">
|
||||
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
|
||||
<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 class="absolute bottom-0 left-0 z-20 px-4 pb-2">
|
||||
<h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2>
|
||||
|
@ -10,6 +10,16 @@
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
|
||||
<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 ?>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div class="flex mt-2 mb-2">
|
||||
<img src="<?= $persons['thumbnail_url'] ?>" alt="<?= $persons[
|
||||
'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">
|
||||
<span class="text-lg font-semibold text-skin-muted md:text-xl">
|
||||
<?= $persons['full_name'] ?>
|
||||
|
@ -15,6 +15,16 @@
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
|
||||
<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')
|
||||
->asset('styles/index.css', 'css') ?>
|
||||
|
@ -10,7 +10,17 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<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 ?>
|
||||
|
||||
@ -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 ?>');">
|
||||
<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">
|
||||
<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">
|
||||
<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', [
|
||||
|
@ -20,5 +20,5 @@ $navigationItems = [
|
||||
<?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>
|
||||
<?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>
|
@ -25,7 +25,7 @@
|
||||
<div class="inline-flex flex-row-reverse">
|
||||
<?php $i = 0; ?>
|
||||
<?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) {
|
||||
break;
|
||||
}?>
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<img src="<?= 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">
|
||||
<Forms.Textarea
|
||||
name="message"
|
||||
|
@ -10,7 +10,17 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<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 ?>
|
||||
|
||||
@ -27,10 +37,10 @@
|
||||
'Fediverse.follow.subtitle',
|
||||
) ?></h1>
|
||||
<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">
|
||||
<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">
|
||||
<p class="font-semibold"><?= $actor->display_name ?></p>
|
||||
<p class="text-sm text-skin-muted">@<?= $actor->username ?></p>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<article class="relative z-10 w-full shadow bg-elevated sm:rounded-conditional-2xl">
|
||||
<header class="flex px-6 py-4 gap-x-2">
|
||||
<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">
|
||||
<a href="<?= $post->actor
|
||||
->uri ?>" class="flex items-baseline hover:underline" <?= $post
|
||||
|
@ -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" >
|
||||
<img src="<?= 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">
|
||||
<Forms.Textarea
|
||||
name="message"
|
||||
|
@ -8,7 +8,7 @@ if ($preview_card->type === 'image'): ?>
|
||||
'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',
|
||||
) ?>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -36,7 +36,7 @@ if ($preview_card->type === 'image'): ?>
|
||||
<?php else: ?>
|
||||
<a href="<?= $preview_card->url ?>" class="flex items-center bg-highlight">
|
||||
<?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; ?>
|
||||
<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>
|
||||
|
@ -8,7 +8,7 @@
|
||||
]) ?></p>
|
||||
<header class="flex px-6 py-4 gap-x-2">
|
||||
<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">
|
||||
<a href="<?= $post->actor
|
||||
->uri ?>" class="flex items-baseline hover:underline" <?= $post
|
||||
|
@ -1,6 +1,6 @@
|
||||
<article class="flex px-6 py-4 bg-base gap-x-2">
|
||||
<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">
|
||||
<header class="flex items-center mb-2">
|
||||
<a href="<?= $reply->actor
|
||||
|
@ -8,7 +8,17 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<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 ?>
|
||||
|
||||
|
@ -11,7 +11,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="<?= service('settings')
|
||||
->get('App.siteIcon')['ico'] ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
|
||||
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
|
||||
<?= service('vite')
|
||||
->asset('styles/index.css', 'css') ?>
|
||||
<?= service('vite')
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import { ManifestCSS } from "./vite-manifest-css";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@ -24,5 +25,11 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [ManifestCSS()],
|
||||
plugins: [
|
||||
ManifestCSS(),
|
||||
VitePWA({
|
||||
manifest: false,
|
||||
outDir: "../../public",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|