6
.gitignore
vendored
@ -143,16 +143,20 @@ node_modules
|
||||
|
||||
# public folder
|
||||
public/*
|
||||
public/media/site
|
||||
!public/media
|
||||
!public/.htaccess
|
||||
!public/favicon.ico
|
||||
!public/icon*
|
||||
!public/castopod-banner*
|
||||
!public/castopod-avatar*
|
||||
!public/index.php
|
||||
!public/robots.txt
|
||||
!public/.well-known
|
||||
!public/.well-known/GDPR.yml
|
||||
|
||||
public/assets/*
|
||||
!public/assets/index.html
|
||||
|
||||
# public media folder
|
||||
!public/media/podcasts
|
||||
!public/media/persons
|
||||
|
@ -23,7 +23,7 @@ class Fediverse extends FediverseBaseConfig
|
||||
*/
|
||||
public string $noteObject = NoteObject::class;
|
||||
|
||||
public string $defaultAvatarImagePath = 'media/castopod-avatar_thumbnail.webp';
|
||||
public string $defaultAvatarImagePath = 'castopod-avatar_thumbnail.webp';
|
||||
|
||||
public string $defaultAvatarImageMimetype = 'image/webp';
|
||||
|
||||
@ -52,7 +52,7 @@ class Fediverse extends FediverseBaseConfig
|
||||
|
||||
helper('media');
|
||||
|
||||
$this->defaultCoverImagePath = media_path($defaultBannerPath . '_federation.' . $extension);
|
||||
$this->defaultCoverImagePath = $defaultBannerPath . '_federation.' . $extension;
|
||||
$this->defaultCoverImageMimetype = $defaultBanner['mimetype'];
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class CreditsController extends BaseController
|
||||
$personId => [
|
||||
'full_name' => $credit->person->full_name,
|
||||
'thumbnail_url' =>
|
||||
$credit->person->avatar->thumbnail_url,
|
||||
get_avatar_url($credit->person, 'thumbnail'),
|
||||
'information_url' =>
|
||||
$credit->person->information_url,
|
||||
'roles' => [
|
||||
@ -90,7 +90,7 @@ class CreditsController extends BaseController
|
||||
$credits[$personGroup]['persons'][$personId] = [
|
||||
'full_name' => $credit->person->full_name,
|
||||
'thumbnail_url' =>
|
||||
$credit->person->avatar->thumbnail_url,
|
||||
get_avatar_url($credit->person, 'thumbnail'),
|
||||
'information_url' => $credit->person->information_url,
|
||||
'roles' => [
|
||||
$personRole => [
|
||||
|
@ -23,7 +23,7 @@ class FeedController extends Controller
|
||||
{
|
||||
public function index(string $podcastHandle): ResponseInterface
|
||||
{
|
||||
helper(['rss', 'premium_podcasts']);
|
||||
helper(['rss', 'premium_podcasts', 'misc']);
|
||||
|
||||
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
|
||||
->first();
|
||||
|
@ -61,14 +61,12 @@ class WebmanifestController extends Controller
|
||||
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
|
||||
'icons' => [
|
||||
[
|
||||
'src' => service('settings')
|
||||
->get('App.siteIcon')['192'],
|
||||
'src' => get_site_icon_url('192'),
|
||||
'type' => 'image/png',
|
||||
'sizes' => '192x192',
|
||||
],
|
||||
[
|
||||
'src' => service('settings')
|
||||
->get('App.siteIcon')['512'],
|
||||
'src' => get_site_icon_url('512'),
|
||||
'type' => 'image/png',
|
||||
'sizes' => '512x512',
|
||||
],
|
||||
|
@ -84,21 +84,10 @@ class Person extends Entity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvatar(): Image
|
||||
public function getAvatar(): ?Image
|
||||
{
|
||||
if ($this->attributes['avatar_id'] === null) {
|
||||
helper('media');
|
||||
return new Image([
|
||||
'file_key' => config('Images')
|
||||
->avatarDefaultPath,
|
||||
'file_mimetype' => config('Images')
|
||||
->avatarDefaultMimeType,
|
||||
'file_size' => 0,
|
||||
'file_metadata' => [
|
||||
'sizes' => config('Images')
|
||||
->personAvatarSizes,
|
||||
],
|
||||
], 'fs');
|
||||
if ($this->avatar_id === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->avatar === null) {
|
||||
|
@ -294,22 +294,10 @@ class Podcast extends Entity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBanner(): Image
|
||||
public function getBanner(): ?Image
|
||||
{
|
||||
if ($this->banner_id === null) {
|
||||
$defaultBanner = config('Images')
|
||||
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
|
||||
'Images'
|
||||
)->podcastBannerDefaultPaths['default'];
|
||||
return new Image([
|
||||
'file_key' => $defaultBanner['path'],
|
||||
'file_mimetype' => $defaultBanner['mimetype'],
|
||||
'file_size' => 0,
|
||||
'file_metadata' => [
|
||||
'sizes' => config('Images')
|
||||
->podcastBannerSizes,
|
||||
],
|
||||
], 'fs');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $this->banner instanceof Image) {
|
||||
|
@ -33,7 +33,7 @@ if (! function_exists('write_audio_file_tags')) {
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
||||
$APICdata = $fileManager->getFileContents($episode->cover->id3_key);
|
||||
$APICdata = (string) $fileManager->getFileContents($episode->cover->id3_key);
|
||||
|
||||
// TODO: variables used for podcast specific tags
|
||||
// $podcastUrl = $episode->podcast->link;
|
||||
|
@ -2,6 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Entities\Person;
|
||||
use App\Entities\Podcast;
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
@ -297,3 +300,89 @@ if (! function_exists('format_bytes')) {
|
||||
return round($bytes, $precision) . $units[$pow];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (! function_exists('get_site_icon_url')) {
|
||||
function get_site_icon_url(string $size): string
|
||||
{
|
||||
if (config('App')->siteIcon['ico'] === service('settings')->get('App.siteIcon')['ico']) {
|
||||
// return default site icon url
|
||||
return base_url(service('settings')->get('App.siteIcon')[$size]);
|
||||
}
|
||||
|
||||
return service('file_manager')->getUrl(service('settings')->get('App.siteIcon')[$size]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (! function_exists('get_podcast_banner')) {
|
||||
function get_podcast_banner_url(Podcast $podcast, string $size): string
|
||||
{
|
||||
if ($podcast->banner === null) {
|
||||
$defaultBanner = config('Images')
|
||||
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
|
||||
'Images'
|
||||
)->podcastBannerDefaultPaths['default'];
|
||||
|
||||
$sizes = config('Images')
|
||||
->podcastBannerSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
helper('filesystem');
|
||||
|
||||
// return default site icon url
|
||||
return base_url(
|
||||
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null)
|
||||
);
|
||||
}
|
||||
|
||||
$sizeKey = $size . '_url';
|
||||
return $podcast->banner->{$sizeKey};
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_podcast_banner_mimetype')) {
|
||||
function get_podcast_banner_mimetype(Podcast $podcast, string $size): string
|
||||
{
|
||||
if ($podcast->banner === null) {
|
||||
$sizes = config('Images')
|
||||
->podcastBannerSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
helper('filesystem');
|
||||
|
||||
// return default site icon url
|
||||
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
|
||||
'Images'
|
||||
)->podcastBannerDefaultMimeType;
|
||||
}
|
||||
|
||||
$mimetype = $size . '_mimetype';
|
||||
return $podcast->banner->{$mimetype};
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_avatar_url')) {
|
||||
function get_avatar_url(Person $person, string $size): string
|
||||
{
|
||||
if ($person->avatar === null) {
|
||||
$defaultAvatar = config('Images')
|
||||
->avatarDefaultPath;
|
||||
|
||||
$sizes = config('Images')
|
||||
->personAvatarSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
|
||||
helper('filesystem');
|
||||
|
||||
// return default site icon url
|
||||
return base_url(
|
||||
change_file_path($defaultAvatar['path'], '_' . $size, $sizeConfig['extension'] ?? null)
|
||||
);
|
||||
}
|
||||
|
||||
$sizeKey = $size . '_url';
|
||||
return $person->avatar->{$sizeKey};
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ if (! function_exists('get_rss_feed')) {
|
||||
foreach ($person->roles as $role) {
|
||||
$personElement = $channel->addChild('person', $person->full_name, $podcastNamespace);
|
||||
|
||||
$personElement->addAttribute('img', $person->avatar->medium_url);
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
|
||||
|
||||
if ($person->information_url !== null) {
|
||||
$personElement->addAttribute('href', $person->information_url);
|
||||
@ -388,7 +388,7 @@ if (! function_exists('get_rss_feed')) {
|
||||
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
|
||||
);
|
||||
|
||||
$personElement->addAttribute('img', $person->avatar->medium_url);
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
|
||||
|
||||
if ($person->information_url !== null) {
|
||||
$personElement->addAttribute('href', $person->information_url);
|
||||
|
@ -273,7 +273,7 @@ if (! function_exists('get_home_metatags')) {
|
||||
$metatags
|
||||
->title(service('settings')->get('App.siteName'))
|
||||
->description(esc(service('settings')->get('App.siteDescription')))
|
||||
->image(service('settings')->get('App.siteIcon')['512'])
|
||||
->image(get_site_icon_url('512'))
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
@ -292,7 +292,7 @@ if (! function_exists('get_page_metatags')) {
|
||||
)->get('App.siteName')
|
||||
)
|
||||
->description(esc(service('settings')->get('App.siteDescription')))
|
||||
->image(service('settings')->get('App.siteIcon')['512'])
|
||||
->image(get_site_icon_url('512'))
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
|
@ -134,7 +134,7 @@ class VideoClipper
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
||||
$jsonTranscriptString = $fileManager->getFileContents($jsonFileKey);
|
||||
$jsonTranscriptString = (string) $fileManager->getFileContents($jsonFileKey);
|
||||
if ($jsonTranscriptString === '') {
|
||||
throw new Exception('Cannot get transcript json contents.');
|
||||
}
|
||||
|
@ -469,8 +469,8 @@ class PodcastModel extends Model
|
||||
$actor->summary = $podcast->description_html;
|
||||
$actor->avatar_image_url = $podcast->cover->federation_url;
|
||||
$actor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
|
||||
$actor->cover_image_url = $podcast->banner->federation_url;
|
||||
$actor->cover_image_mimetype = $podcast->banner->federation_mimetype;
|
||||
$actor->cover_image_url = get_podcast_banner_url($podcast, 'federation');
|
||||
$actor->cover_image_mimetype = get_podcast_banner_mimetype($podcast, 'federation');
|
||||
|
||||
if ($actor->hasChanged()) {
|
||||
$actorModel->update($actor->id, $actor);
|
||||
|
@ -17,10 +17,10 @@ use App\Models\EpisodeModel;
|
||||
use App\Models\PersonModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\PostModel;
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use Modules\Media\Entities\Audio;
|
||||
use Modules\Media\FileManagers\FileManagerInterface;
|
||||
use Modules\Media\FileManagers\FS;
|
||||
use Modules\Media\Models\MediaModel;
|
||||
use PHP_ICO;
|
||||
|
||||
@ -58,47 +58,60 @@ class SettingsController extends BaseController
|
||||
|
||||
$siteIconFile = $this->request->getFile('site_icon');
|
||||
if ($siteIconFile !== null && $siteIconFile->isValid()) {
|
||||
helper(['filesystem', 'media']);
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
||||
// delete site folder in media before repopulating it
|
||||
delete_files(media_path_absolute('/site'));
|
||||
|
||||
// save original in disk
|
||||
$originalFilename = (new FS(config('Media')))->save(
|
||||
$siteIconFile,
|
||||
'site/icon.' . $siteIconFile->getExtension()
|
||||
);
|
||||
$fileManager->deleteAll('site');
|
||||
|
||||
// convert jpeg image to png if not
|
||||
if ($siteIconFile->getClientMimeType() !== 'image/png') {
|
||||
service('image')->withFile(media_path_absolute($originalFilename))
|
||||
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
|
||||
service('image')
|
||||
->withFile($siteIconFile->getRealPath())
|
||||
->convert(IMAGETYPE_JPEG)
|
||||
->save(media_path_absolute('/site/icon.png'));
|
||||
->save($tempFilePath);
|
||||
|
||||
@unlink($siteIconFile->getRealPath());
|
||||
|
||||
$siteIconFile = new File($tempFilePath, true);
|
||||
}
|
||||
|
||||
$icoTempFilePath = WRITEPATH . 'temp/img_favicon.ico';
|
||||
|
||||
// generate ico
|
||||
$ico_lib = new PHP_ICO();
|
||||
$ico_lib->add_image($siteIconFile->getRealPath(), [[32, 32], [64, 64]]);
|
||||
$ico_lib->save_ico($icoTempFilePath);
|
||||
|
||||
// generate random hash to use as a suffix to renew browser cache
|
||||
$randomHash = substr(bin2hex(random_bytes(18)), 0, 8);
|
||||
|
||||
// generate ico
|
||||
$ico_lib = new PHP_ICO();
|
||||
$ico_lib->add_image(media_path_absolute('/site/icon.png'), [[32, 32], [64, 64]]);
|
||||
$ico_lib->save_ico(media_path_absolute("/site/favicon.{$randomHash}.ico"));
|
||||
// save ico
|
||||
$fileManager->save(new File($icoTempFilePath, true), "site/favicon.{$randomHash}.ico");
|
||||
|
||||
// resize original to needed sizes
|
||||
foreach ([64, 180, 192, 512] as $size) {
|
||||
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
|
||||
service('image')
|
||||
->withFile(media_path_absolute('/site/icon.png'))
|
||||
->withFile($siteIconFile->getRealPath())
|
||||
->resize($size, $size)
|
||||
->save(media_path_absolute("/site/icon-{$size}.{$randomHash}.png"));
|
||||
->save($tempFilePath);
|
||||
|
||||
// save sizes to
|
||||
$fileManager->save(new File($tempFilePath), "site/icon-{$size}.{$randomHash}.png");
|
||||
}
|
||||
|
||||
// save original as png
|
||||
$fileManager->save($siteIconFile, 'site/icon.png');
|
||||
|
||||
service('settings')
|
||||
->set('App.siteIcon', [
|
||||
'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"),
|
||||
'ico' => "site/favicon.{$randomHash}.ico",
|
||||
'64' => "site/icon-64.{$randomHash}.png",
|
||||
'180' => "site/icon-180.{$randomHash}.png",
|
||||
'192' => "site/icon-192.{$randomHash}.png",
|
||||
'512' => "site/icon-512.{$randomHash}.png",
|
||||
]);
|
||||
}
|
||||
|
||||
@ -107,9 +120,11 @@ class SettingsController extends BaseController
|
||||
|
||||
public function deleteIcon(): RedirectResponse
|
||||
{
|
||||
helper(['filesystem', 'media']);
|
||||
// delete site folder in media
|
||||
delete_files(media_path_absolute('/site'));
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
||||
// delete site folder
|
||||
$fileManager->deleteAll('site');
|
||||
|
||||
service('settings')
|
||||
->forget('App.siteIcon');
|
||||
|
@ -30,11 +30,11 @@ class Fediverse extends BaseConfig
|
||||
* Default avatar and cover images
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
public string $defaultAvatarImagePath = 'media/avatar-default.jpg';
|
||||
public string $defaultAvatarImagePath = 'avatar-default.jpg';
|
||||
|
||||
public string $defaultAvatarImageMimetype = 'image/jpeg';
|
||||
|
||||
public string $defaultCoverImagePath = 'media/banner-default.jpg';
|
||||
public string $defaultCoverImagePath = 'banner-default.jpg';
|
||||
|
||||
public string $defaultCoverImageMimetype = 'image/jpeg';
|
||||
|
||||
|
16
modules/Media/Config/Routes.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
$routes = service('routes');
|
||||
|
||||
$routes->get('static/(:any)', 'MediaController::serve/$1', [
|
||||
'as' => 'media-serve',
|
||||
'namespace' => 'Modules\Media\Controllers',
|
||||
]);
|
26
modules/Media/Controllers/MediaController.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace Modules\Media\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use Modules\Media\FileManagers\FileManagerInterface;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function serve(string ...$key): Response
|
||||
{
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
||||
return $fileManager->serve(implode('/', $key));
|
||||
}
|
||||
}
|
@ -12,9 +12,6 @@ namespace Modules\Media\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\Files\File;
|
||||
use Modules\Media\FileManagers\FileManagerInterface;
|
||||
use Modules\Media\FileManagers\FS;
|
||||
use Modules\Media\FileManagers\S3;
|
||||
use Modules\Media\Models\MediaModel;
|
||||
|
||||
/**
|
||||
@ -58,28 +55,13 @@ class BaseMedia extends Entity
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
protected FileManagerInterface $fileManager;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
* @param 'fs'|'s3'|null $fileManager
|
||||
*/
|
||||
public function __construct(array $data = null, string $fileManager = null)
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
|
||||
if ($fileManager !== null) {
|
||||
$this->fileManager = match ($fileManager) {
|
||||
'fs' => new FS(config('Media')),
|
||||
's3' => new S3(config('Media'))
|
||||
};
|
||||
} else {
|
||||
/** @var FileManagerInterface $fileManagerService */
|
||||
$fileManagerService = service('file_manager');
|
||||
|
||||
$this->fileManager = $fileManagerService;
|
||||
}
|
||||
|
||||
$this->initFileProperties();
|
||||
}
|
||||
|
||||
@ -92,7 +74,7 @@ class BaseMedia extends Entity
|
||||
'extension' => $extension,
|
||||
] = pathinfo($this->file_key);
|
||||
|
||||
$this->attributes['file_url'] = $this->fileManager->getUrl($this->file_key);
|
||||
$this->attributes['file_url'] = service('file_manager')->getUrl($this->file_key);
|
||||
$this->attributes['file_name'] = $filename;
|
||||
$this->attributes['file_directory'] = $dirname;
|
||||
$this->attributes['file_extension'] = $extension;
|
||||
@ -120,14 +102,14 @@ class BaseMedia extends Entity
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->attributes['file_key'] = $this->fileManager->save($this->attributes['file'], $this->file_key);
|
||||
$this->attributes['file_key'] = service('file_manager')->save($this->attributes['file'], $this->file_key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteFile(): bool
|
||||
{
|
||||
return $this->fileManager->delete($this->file_key);
|
||||
return service('file_manager')->delete($this->file_key);
|
||||
}
|
||||
|
||||
public function rename(): bool
|
||||
@ -143,7 +125,7 @@ class BaseMedia extends Entity
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->fileManager->rename($this->file_key, $newFileKey)) {
|
||||
if (! service('file_manager')->rename($this->file_key, $newFileKey)) {
|
||||
$db->transRollback();
|
||||
return false;
|
||||
}
|
||||
|
@ -42,14 +42,12 @@ class Image extends BaseMedia
|
||||
{
|
||||
helper('filesystem');
|
||||
|
||||
$fileKeyWithoutExt = path_without_ext($this->file_key);
|
||||
|
||||
foreach ($this->sizes as $name => $size) {
|
||||
$extension = array_key_exists('extension', $size) ? $size['extension'] : $this->file_extension;
|
||||
$mimetype = array_key_exists('mimetype', $size) ? $size['mimetype'] : $this->file_mimetype;
|
||||
|
||||
$this->{$name . '_key'} = $fileKeyWithoutExt . '_' . $name . '.' . $extension;
|
||||
$this->{$name . '_url'} = $this->fileManager->getUrl($this->{$name . '_key'});
|
||||
$this->{$name . '_key'} = change_file_path($this->file_key, '_' . $name, $extension);
|
||||
$this->{$name . '_url'} = service('file_manager')->getUrl($this->{$name . '_key'});
|
||||
$this->{$name . '_mimetype'} = $mimetype;
|
||||
}
|
||||
|
||||
@ -126,7 +124,8 @@ class Image extends BaseMedia
|
||||
|
||||
// download image temporarily to generate sizes from
|
||||
$tempImagePath = (string) tempnam(WRITEPATH . 'temp', 'img_');
|
||||
$imageContent = $this->fileManager->getFileContents($this->file_key);
|
||||
$imageContent = (string) service('file_manager')
|
||||
->getFileContents($this->file_key);
|
||||
file_put_contents($tempImagePath, $imageContent);
|
||||
|
||||
$this->attributes['file'] = new File($tempImagePath, true);
|
||||
@ -144,7 +143,7 @@ class Image extends BaseMedia
|
||||
|
||||
$newImage = new File($tempFilePath, true);
|
||||
|
||||
$this->fileManager
|
||||
service('file_manager')
|
||||
->save($newImage, $this->{$name . '_key'});
|
||||
}
|
||||
|
||||
@ -159,7 +158,7 @@ class Image extends BaseMedia
|
||||
foreach (array_keys($this->sizes) as $name) {
|
||||
$pathProperty = $name . '_key';
|
||||
|
||||
if (! $this->fileManager->delete($this->{$pathProperty})) {
|
||||
if (! service('file_manager')->delete($this->{$pathProperty})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class Transcript extends BaseMedia
|
||||
helper('media');
|
||||
|
||||
$this->json_key = $this->file_metadata['json_key'];
|
||||
$this->json_url = $this->fileManager
|
||||
$this->json_url = service('file_manager')
|
||||
->getUrl($this->json_key);
|
||||
}
|
||||
}
|
||||
@ -42,12 +42,8 @@ class Transcript extends BaseMedia
|
||||
|
||||
helper('filesystem');
|
||||
|
||||
$fileKeyWithoutExt = path_without_ext($this->file_key);
|
||||
|
||||
$jsonfileKey = $fileKeyWithoutExt . '.json';
|
||||
|
||||
// set metadata (generated json file path)
|
||||
$this->json_key = $jsonfileKey;
|
||||
$this->json_key = change_file_path($this->file_key, '', 'json');
|
||||
$metadata['json_key'] = $this->json_key;
|
||||
|
||||
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
|
||||
@ -71,7 +67,7 @@ class Transcript extends BaseMedia
|
||||
}
|
||||
|
||||
if ($this->json_key) {
|
||||
return $this->fileManager->delete($this->json_key);
|
||||
return service('file_manager')->delete($this->json_key);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -96,7 +92,7 @@ class Transcript extends BaseMedia
|
||||
|
||||
$newTranscriptJson = new File($tempFilePath, true);
|
||||
|
||||
$this->fileManager
|
||||
service('file_manager')
|
||||
->save($newTranscriptJson, $this->json_key);
|
||||
|
||||
return true;
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Media\FileManagers;
|
||||
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use Exception;
|
||||
use Modules\Media\Config\Media as MediaConfig;
|
||||
|
||||
@ -27,7 +28,7 @@ class FS implements FileManagerInterface
|
||||
$path = $path . '.' . $extension;
|
||||
}
|
||||
|
||||
$mediaRoot = media_path_absolute();
|
||||
$mediaRoot = $this->media_path_absolute();
|
||||
|
||||
if (! file_exists(dirname($mediaRoot . '/' . $path))) {
|
||||
mkdir(dirname($mediaRoot . '/' . $path), 0777, true);
|
||||
@ -51,7 +52,7 @@ class FS implements FileManagerInterface
|
||||
{
|
||||
helper('media');
|
||||
|
||||
return @unlink(media_path_absolute($key));
|
||||
return @unlink($this->media_path_absolute($key));
|
||||
}
|
||||
|
||||
public function getUrl(string $key): string
|
||||
@ -70,21 +71,21 @@ class FS implements FileManagerInterface
|
||||
{
|
||||
helper('media');
|
||||
|
||||
return rename(media_path_absolute($oldKey), media_path_absolute($newKey));
|
||||
return rename($this->media_path_absolute($oldKey), $this->media_path_absolute($newKey));
|
||||
}
|
||||
|
||||
public function getFileContents(string $key): string
|
||||
public function getFileContents(string $key): string|false
|
||||
{
|
||||
helper('media');
|
||||
|
||||
return (string) file_get_contents(media_path_absolute($key));
|
||||
return file_get_contents($this->media_path_absolute($key));
|
||||
}
|
||||
|
||||
public function getFileInput(string $key): string
|
||||
{
|
||||
helper('media');
|
||||
|
||||
return media_path_absolute($key);
|
||||
return $this->media_path_absolute($key);
|
||||
}
|
||||
|
||||
public function deletePodcastImageSizes(string $podcastHandle): bool
|
||||
@ -113,12 +114,12 @@ class FS implements FileManagerInterface
|
||||
if ($pattern === '*') {
|
||||
helper('filesystem');
|
||||
|
||||
return delete_files(media_path_absolute($prefix), true);
|
||||
return delete_files($this->media_path_absolute($prefix), true);
|
||||
}
|
||||
|
||||
$prefix = rtrim($prefix, '/') . '/';
|
||||
|
||||
$imagePaths = glob(media_path_absolute($prefix . $pattern));
|
||||
$imagePaths = glob($this->media_path_absolute($prefix . $pattern));
|
||||
|
||||
if (! $imagePaths) {
|
||||
return true;
|
||||
@ -135,6 +136,28 @@ class FS implements FileManagerInterface
|
||||
{
|
||||
helper('media');
|
||||
|
||||
return is_really_writable(media_path_absolute());
|
||||
return is_really_writable($this->media_path_absolute());
|
||||
}
|
||||
|
||||
public function serve(string $key): Response
|
||||
{
|
||||
return redirect()->to($this->getUrl($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the absolute storage directory to the media path of a given uri
|
||||
*
|
||||
* @param string|string[] $uri URI string or array of URI segments
|
||||
*/
|
||||
private function media_path_absolute(string | array $uri = ''): string
|
||||
{
|
||||
// convert segment array to string
|
||||
if (is_array($uri)) {
|
||||
$uri = implode('/', $uri);
|
||||
}
|
||||
|
||||
$uri = trim($uri, '/');
|
||||
|
||||
return config('Media')->storage . '/' . config('Media')->root . '/' . $uri;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Media\FileManagers;
|
||||
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
|
||||
interface FileManagerInterface
|
||||
{
|
||||
@ -16,7 +17,7 @@ interface FileManagerInterface
|
||||
|
||||
public function rename(string $oldKey, string $newKey): bool;
|
||||
|
||||
public function getFileContents(string $key): string;
|
||||
public function getFileContents(string $key): string|false;
|
||||
|
||||
public function getFileInput(string $key): string;
|
||||
|
||||
@ -27,4 +28,6 @@ interface FileManagerInterface
|
||||
public function deleteAll(string $prefix, string $pattern = '*'): bool;
|
||||
|
||||
public function isHealthy(): bool;
|
||||
|
||||
public function serve(string $key): Response;
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ namespace Modules\Media\FileManagers;
|
||||
|
||||
use Aws\Credentials\Credentials;
|
||||
use Aws\S3\S3Client;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use Exception;
|
||||
use Modules\Media\Config\Media as MediaConfig;
|
||||
|
||||
@ -35,6 +36,7 @@ class S3 implements FileManagerInterface
|
||||
'Bucket' => $this->config->s3['bucket'],
|
||||
'Key' => $this->prefixKey($key),
|
||||
'SourceFile' => $file,
|
||||
'ContentType' => $file->getMimeType(),
|
||||
]);
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
@ -62,16 +64,7 @@ class S3 implements FileManagerInterface
|
||||
|
||||
public function getUrl(string $key): string
|
||||
{
|
||||
$url = new URI((string) $this->config->s3['endpoint']);
|
||||
|
||||
if ($this->config->s3['pathStyleEndpoint'] === true) {
|
||||
$url->setPath($this->config->s3['bucket'] . '/' . $this->prefixKey($key));
|
||||
return (string) $url;
|
||||
}
|
||||
|
||||
$url->setHost($this->config->s3['bucket'] . '.' . $url->getHost());
|
||||
$url->setPath($this->prefixKey($key));
|
||||
return (string) $url;
|
||||
return url_to('media-serve', $key);
|
||||
}
|
||||
|
||||
public function rename(string $oldKey, string $newKey): bool
|
||||
@ -91,12 +84,16 @@ class S3 implements FileManagerInterface
|
||||
return $this->delete($oldKey);
|
||||
}
|
||||
|
||||
public function getFileContents(string $key): string
|
||||
public function getFileContents(string $key): string|false
|
||||
{
|
||||
$result = $this->s3->getObject([
|
||||
'Bucket' => $this->config->s3['bucket'],
|
||||
'Key' => $this->prefixKey($key),
|
||||
]);
|
||||
try {
|
||||
$result = $this->s3->getObject([
|
||||
'Bucket' => $this->config->s3['bucket'],
|
||||
'Key' => $this->prefixKey($key),
|
||||
]);
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $result->get('Body');
|
||||
}
|
||||
@ -186,6 +183,31 @@ class S3 implements FileManagerInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
public function serve(string $key): Response
|
||||
{
|
||||
$response = service('response');
|
||||
|
||||
try {
|
||||
$result = $this->s3->getObject([
|
||||
'Bucket' => $this->config->s3['bucket'],
|
||||
'Key' => $this->prefixKey($key),
|
||||
]);
|
||||
} catch (Exception) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
// Remove Cache-Control header before redefining it
|
||||
header_remove('Cache-Control');
|
||||
|
||||
return $response->setCache([
|
||||
'max-age' => DECADE,
|
||||
'last-modified' => $result->get('LastModified'),
|
||||
'etag' => $result->get('ETag'),
|
||||
'public' => true,
|
||||
])->setContentType($result->get('ContentType'))
|
||||
->setBody((string) $result->get('Body')->getContents());
|
||||
}
|
||||
|
||||
private function prefixKey(string $key): string
|
||||
{
|
||||
if ($this->config->s3['keyPrefix'] === '') {
|
||||
|
@ -8,20 +8,13 @@ declare(strict_types=1);
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
|
||||
if (! function_exists('path_without_ext')) {
|
||||
function path_without_ext(string $path): string
|
||||
if (! function_exists('add_suffix_to_path')) {
|
||||
function change_file_path(string $path, string $suffix = '', ?string $newExtension = null): string
|
||||
{
|
||||
$fileKeyInfo = pathinfo($path);
|
||||
|
||||
if ($fileKeyInfo['dirname'] === '.' && ! str_starts_with($path, '.')) {
|
||||
return $fileKeyInfo['filename'];
|
||||
if ($newExtension === null) {
|
||||
$newExtension = pathinfo($path, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
if ($fileKeyInfo['dirname'] === '/') {
|
||||
return '/' . $fileKeyInfo['filename'];
|
||||
}
|
||||
|
||||
return implode('/', [$fileKeyInfo['dirname'], $fileKeyInfo['filename']]);
|
||||
return preg_replace('~\.[^.]+$~', '', $path) . $suffix . '.' . $newExtension;
|
||||
}
|
||||
}
|
||||
|
@ -63,34 +63,3 @@ if (! function_exists('download_file')) {
|
||||
return new File($tmpfileKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('media_path')) {
|
||||
/**
|
||||
* Prefixes the root media path to a given uri
|
||||
*
|
||||
* @param string|string[] $uri URI string or array of URI segments
|
||||
*/
|
||||
function media_path(string | array $uri = ''): string
|
||||
{
|
||||
// convert segment array to string
|
||||
if (is_array($uri)) {
|
||||
$uri = implode('/', $uri);
|
||||
}
|
||||
|
||||
$uri = trim($uri, '/');
|
||||
|
||||
return config('Media')->root . '/' . $uri;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('media_path_absolute')) {
|
||||
/**
|
||||
* Prefixes the absolute storage directory to the media path of a given uri
|
||||
*
|
||||
* @param string|string[] $uri URI string or array of URI segments
|
||||
*/
|
||||
function media_path_absolute(string | array $uri = ''): string
|
||||
{
|
||||
return config('Media')->storage . '/' . media_path($uri);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Directory access is forbidden.</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Directory access is forbidden.</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Directory access is forbidden.</p>
|
||||
</body>
|
||||
</html>
|
@ -15,9 +15,8 @@ $isEpisodeArea = isset($podcast) && isset($episode);
|
||||
<title><?= $this->renderSection('title') ?> | Castopod Admin</title>
|
||||
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
|
||||
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
|
@ -58,7 +58,7 @@
|
||||
return '<div class="flex">' .
|
||||
'<a href="' .
|
||||
route_to('person-view', $person->id) .
|
||||
'"><img src="' . $person->avatar->thumbnail_url . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 rounded-full aspect-square" loading="lazy" /></a>' .
|
||||
'"><img src="' . get_avatar_url($person, 'thumbnail') . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 rounded-full aspect-square" loading="lazy" /></a>' .
|
||||
'<div class="flex flex-col ml-3">' .
|
||||
esc($person->full_name) .
|
||||
implode(
|
||||
|
@ -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 mix-blend-multiply"></div>
|
||||
<div class="w-full h-full overflow-hidden bg-header">
|
||||
<img alt="<?= esc($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" loading="lazy" />
|
||||
<img alt="<?= esc($person->full_name) ?>" src="<?= get_avatar_url($person, 'medium') ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" loading="lazy" />
|
||||
</div>
|
||||
<div class="absolute z-20">
|
||||
<h2 class="px-4 py-2 font-semibold leading-tight"><?= esc($person->full_name) ?></h2>
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<img
|
||||
src="<?= $person->avatar->medium_url ?>"
|
||||
src="<?= get_avatar_url($person, 'medium') ?>"
|
||||
alt="<?= esc($person->full_name) ?>"
|
||||
class="object-cover w-full max-w-xs rounded aspect-square"
|
||||
loading="lazy"
|
||||
|
@ -25,7 +25,7 @@
|
||||
<?php if ($podcast->banner_id !== null): ?>
|
||||
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast focus:ring-accent top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>" data-tooltip="bottom"><?= icon('delete-bin') ?></a>
|
||||
<?php endif; ?>
|
||||
<img src="<?= $podcast->banner->small_url ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
|
||||
<img src="<?= get_podcast_banner_url($podcast, 'small') ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
|
||||
<div class="flex px-4 py-2">
|
||||
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= esc($podcast->title) ?>"
|
||||
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" loading="lazy" />
|
||||
|
@ -55,7 +55,7 @@
|
||||
return '<div class="flex">' .
|
||||
'<a href="' .
|
||||
route_to('person-view', $person->id) .
|
||||
'"><img src="' . $person->avatar->thumbnail_url . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 h-16 rounded-full aspect-square" loading="lazy" /></a>' .
|
||||
'"><img src="' . get_avatar_url($person, 'thumbnail') . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 h-16 rounded-full aspect-square" loading="lazy" /></a>' .
|
||||
'<div class="flex flex-col ml-3">' .
|
||||
esc($person->full_name) .
|
||||
implode(
|
||||
|
@ -46,7 +46,7 @@
|
||||
<?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.instance.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin') ?></a>
|
||||
<img src="<?= service('settings')->get('App.siteIcon')['64'] ?>" alt="<?= esc(service('settings')->get('App.siteName')) ?> Favicon" class="w-10 h-10 aspect-square" loading="lazy" />
|
||||
<img src="<?= get_site_icon_url('64') ?>" alt="<?= esc(service('settings')->get('App.siteName')) ?> Favicon" class="w-10 h-10 aspect-square" loading="lazy" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
<Forms.Section
|
||||
title="<?= lang('Settings.images.title') ?>"
|
||||
subtitle="<?= lang('Settings.images.subtitle') ?>" >
|
||||
subtitle="<?= lang('Settings.images.subtitle') ?>">
|
||||
|
||||
<Button variant="primary" type="submit" iconLeft="refresh"><?= lang('Settings.images.regenerate') ?></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="<?= esc($person->full_name) ?>" class="object-cover w-10 rounded-full bg-header aspect-square" loading="lazy" />
|
||||
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-10 rounded-full bg-header aspect-square" loading="lazy" />
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-sm font-semibold">
|
||||
<?php if ($person->information_url): ?>
|
||||
|
@ -9,9 +9,8 @@
|
||||
<meta name="description" content="<?= esc(
|
||||
$episode->description,
|
||||
) ?>" />
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')
|
||||
->asset('styles/index.css', 'css') ?>
|
||||
|
@ -7,9 +7,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
@ -78,7 +77,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
<header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-accent-base/75 gap-y-4">
|
||||
<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('<?= get_podcast_banner_url($episode->podcast, 'small') ?>');"></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="relative flex-shrink-0">
|
||||
@ -97,7 +96,7 @@
|
||||
<span class="inline-flex flex-row-reverse">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach ($episode->persons as $person): ?>
|
||||
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
|
||||
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
|
||||
<?php $i++;
|
||||
if ($i === 3) {
|
||||
break;
|
||||
|
@ -6,9 +6,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('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>
|
||||
|
@ -6,9 +6,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('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>
|
||||
|
@ -11,9 +11,8 @@
|
||||
->get('App.siteName')),
|
||||
]) ?>"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('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>
|
||||
|
@ -7,9 +7,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
@ -43,7 +42,7 @@
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<header class="min-h-[200px] relative z-50 flex flex-col-reverse justify-between w-full gap-x-2 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="min-h-[200px] relative z-50 flex flex-col-reverse justify-between w-full gap-x-2 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('<?= get_podcast_banner_url($podcast, 'medium') ?>');">
|
||||
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient mix-blend-multiply"></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="<?= esc($podcast->title) ?>" class="h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated aspect-square" loading="lazy" />
|
||||
|
@ -21,7 +21,7 @@
|
||||
<span class="inline-flex flex-row-reverse">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach ($podcast->persons as $person): ?>
|
||||
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 -ml-4 border-2 rounded-full aspect-square bg-header border-background-base last:ml-0" loading="lazy" />
|
||||
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 -ml-4 border-2 rounded-full aspect-square bg-header border-background-base last:ml-0" loading="lazy" />
|
||||
<?php $i++;
|
||||
if ($i === 3) {
|
||||
break;
|
||||
|
@ -7,9 +7,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($actor->podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
@ -38,7 +37,7 @@
|
||||
'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="w-full aspect-[3/1] bg-header" loading="lazy" />
|
||||
<img src="<?= get_podcast_banner_url($actor->podcast, 'small') ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
|
||||
<div class="flex px-4 py-2">
|
||||
<img src="<?= $actor->avatar_image_url ?>" alt="<?= esc($actor->display_name) ?>"
|
||||
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" loading="lazy" />
|
||||
|
@ -7,9 +7,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
@ -70,7 +69,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<header class="relative 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 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('<?= get_podcast_banner_url($podcast, 'medium') ?>');">
|
||||
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient mix-blend-multiply"></div>
|
||||
<div class="flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
|
||||
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= esc($podcast->title) ?>" class="z-[45] h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated aspect-square" loading="lazy" />
|
||||
|
@ -5,9 +5,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($post->actor->podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
|