Compare commits

..

4 Commits

Author SHA1 Message Date
Yassine Doghri 964723cd52 Merge branch 'chore/update-ci' into 'develop'
chore: update codeigniter to 4.5.1 + other dependencies to latest

See merge request adaures/castopod!342
2024-04-24 17:29:42 +00:00
Yassine Doghri 0acc1363f0 chore: change rector and ecs configs + update devcontainer to php 8.2 2024-04-24 17:24:57 +00:00
Yassine Doghri f9a939471d chore: update codeigniter to 4.5.1 + other dependencies to latest 2024-04-24 15:42:17 +00:00
Yassine Doghri 303a900f66 refactor(platforms): move platforms data in code instead of database
refs #457
2024-04-24 14:47:05 +00:00
75 changed files with 9773 additions and 7919 deletions

View File

@ -4,7 +4,7 @@
# ⚠️ NOT optimized for production # ⚠️ NOT optimized for production
# should be used only for development purposes # should be used only for development purposes
#--------------------------------------------------- #---------------------------------------------------
FROM php:8.1-fpm FROM php:8.2-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>" LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"

View File

@ -83,7 +83,7 @@ class App extends BaseConfig
* DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! * DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
* *
*/ */
public string $permittedURIChars = 'a-z 0-9~%.:_\-'; public string $permittedURIChars = 'a-z 0-9~%.:_\-@';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------

View File

@ -49,6 +49,7 @@ class Autoload extends AutoloadConfig
'Modules\Install' => ROOTPATH . 'modules/Install/', 'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Media' => ROOTPATH . 'modules/Media/', 'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/', 'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/', 'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/', 'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
'Modules\Update' => ROOTPATH . 'modules/Update/', 'Modules\Update' => ROOTPATH . 'modules/Update/',
@ -61,7 +62,6 @@ class Autoload extends AutoloadConfig
/** /**
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* Class Map
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* The class map provides a map of class names and their exact * The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have * location on the drive. Classes loaded in this manner will have

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Config; namespace Config;
/** /**

View File

@ -15,7 +15,6 @@ use CodeIgniter\Router\RouteCollection;
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}'); $routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}'); $routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}'); $routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply'); $routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent'); $routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder( $routes->addPlaceholder(

View File

@ -29,6 +29,7 @@ class Routing extends BaseRouting
ROOTPATH . 'modules/Auth/Config/Routes.php', ROOTPATH . 'modules/Auth/Config/Routes.php',
ROOTPATH . 'modules/Fediverse/Config/Routes.php', ROOTPATH . 'modules/Fediverse/Config/Routes.php',
ROOTPATH . 'modules/Install/Config/Routes.php', ROOTPATH . 'modules/Install/Config/Routes.php',
ROOTPATH . 'modules/Platforms/Config/Routes.php',
ROOTPATH . 'modules/PodcastImport/Config/Routes.php', ROOTPATH . 'modules/PodcastImport/Config/Routes.php',
ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php', ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php',
]; ];

View File

@ -37,8 +37,8 @@ class Services extends BaseService
return static::getSharedInstance('router', $routes, $request); return static::getSharedInstance('router', $routes, $request);
} }
$routes = $routes ?? static::routes(); $routes ??= static::routes();
$request = $request ?? static::request(); $request ??= static::request();
return new Router($routes, $request); return new Router($routes, $request);
} }
@ -53,7 +53,7 @@ class Services extends BaseService
return static::getSharedInstance('negotiator', $request); return static::getSharedInstance('negotiator', $request);
} }
$request = $request ?? static::request(); $request ??= static::request();
return new Negotiate($request); return new Negotiate($request);
} }

View File

@ -13,8 +13,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all * BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. Extend this class in any new controllers: class Home extends BaseController * your controllers. Extend this class in any new controllers: class Home extends BaseController
* *

View File

@ -106,9 +106,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [ return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -157,9 +155,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [ return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -218,9 +214,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [ return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -284,9 +278,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [ return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -339,9 +331,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [ return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -420,11 +410,9 @@ class EpisodeController extends BaseController
* get comments: aggregated replies from posts referring to the episode * get comments: aggregated replies from posts referring to the episode
*/ */
$episodeComments = model(PostModel::class) $episodeComments = model(PostModel::class)
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder { ->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
return $builder->select('id') ->from('fediverse_posts')
->from('fediverse_posts') ->where('episode_id', $this->episode->id))
->where('episode_id', $this->episode->id);
})
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');

View File

@ -79,13 +79,7 @@ class FeedController extends Controller
); );
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $this->response->setXML($found); return $this->response->setXML($found);

View File

@ -1,28 +0,0 @@
<?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 App\Controllers;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
/*
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{
public function index(): ResponseInterface
{
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}

View File

@ -96,9 +96,7 @@ class PodcastController extends BaseController
); );
return view('podcast/activity', $data, [ return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -148,9 +146,7 @@ class PodcastController extends BaseController
); );
return view('podcast/about', $data, [ return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -270,9 +266,7 @@ class PodcastController extends BaseController
$this->podcast->id, $this->podcast->id,
); );
return view('podcast/episodes', $data, [ return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RefactorPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
'after' => 'podcast_id',
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE', 'platforms_podcast_id_foreign');
$this->forge->addUniqueKey(['podcast_id', 'type', 'slug']);
$this->forge->createTable('platforms_temp');
$platformsData = $this->db->table('podcasts_platforms')
->select('podcasts_platforms.*, type')
->join('platforms', 'platforms.slug = podcasts_platforms.platform_slug')
->get()
->getResultArray();
$data = [];
foreach ($platformsData as $platformData) {
$data[] = [
'podcast_id' => $platformData['podcast_id'],
'type' => $platformData['type'],
'slug' => $platformData['platform_slug'],
'link_url' => $platformData['link_url'],
'account_id' => $platformData['account_id'],
'is_visible' => $platformData['is_visible'],
];
}
if ($data !== []) {
$this->db->table('platforms_temp')
->insertBatch($data);
}
$this->forge->dropTable('platforms');
$this->forge->dropTable('podcasts_platforms');
$this->forge->renameTable('platforms_temp', 'platforms');
}
public function down(): void
{
// delete platforms
$this->forge->dropTable('platforms');
// recreate platforms and podcasts_platforms tables
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()'
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug', 'CASCADE');
$this->forge->createTable('podcasts_platforms');
}
}

View File

@ -20,6 +20,5 @@ class AppSeeder extends Seeder
{ {
$this->call('CategorySeeder'); $this->call('CategorySeeder');
$this->call('LanguageSeeder'); $this->call('LanguageSeeder');
$this->call('PlatformSeeder');
} }
} }

View File

@ -20,7 +20,6 @@ class DevSeeder extends Seeder
{ {
$this->call('CategorySeeder'); $this->call('CategorySeeder');
$this->call('LanguageSeeder'); $this->call('LanguageSeeder');
$this->call('PlatformSeeder');
$this->call('DevSuperadminSeeder'); $this->call('DevSuperadminSeeder');
} }
} }

View File

@ -77,34 +77,32 @@ class FakePodcastsAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, (int) $probability1); $lineNumber < random_int(1, (int) $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = floor(exp(6 - $age / 20)) + 10; $probability2 = floor(exp(6 - $age / 20)) + 10;
$player = $player =
$jsonUserAgents[ $jsonUserAgents[
rand(1, count($jsonUserAgents) - 1) random_int(1, count($jsonUserAgents) - 1)
]; ];
$service = $service =
$jsonRSSUserAgents[ $jsonRSSUserAgents[
rand(1, count($jsonRSSUserAgents) - 1) random_int(1, count($jsonRSSUserAgents) - 1)
]['slug']; ]['slug'];
$app = isset($player['app']) ? $player['app'] : ''; $app = $player['app'] ?? '';
$device = isset($player['device']) $device = $player['device'] ?? '';
? $player['device'] $os = $player['os'] ?? '';
: ''; $isBot = $player['bot'] ?? 0;
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
$fakeIp = $fakeIp =
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255); random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'); $cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
@ -115,9 +113,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
try { try {
$city = $cityReader->city($fakeIp); $city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null $countryCode = $city->country->isoCode ?? 'N/A';
? 'N/A'
: $city->country->isoCode;
$regionCode = $city->subdivisions === [] $regionCode = $city->subdivisions === []
? 'N/A' ? 'N/A'
@ -128,20 +124,20 @@ class FakePodcastsAnalyticsSeeder extends Seeder
//Bad luck, bad IP, nothing to do. //Bad luck, bad IP, nothing to do.
} }
$hits = rand(0, (int) $probability2); $hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [ $analyticsPodcasts[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600), 'duration' => random_int(60, 3600),
'bandwidth' => rand(1000000, 10000000), 'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits, 'hits' => $hits,
'unique_listeners' => $hits, 'unique_listeners' => $hits,
]; ];
$analyticsPodcastsByHour[] = [ $analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'hour' => rand(0, 23), 'hour' => random_int(0, 23),
'hits' => $hits, 'hits' => $hits,
]; ];
$analyticsPodcastsByCountry[] = [ $analyticsPodcastsByCountry[] = [

View File

@ -216,23 +216,23 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, $probability1); $lineNumber < random_int(1, $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$domain = $domain =
$this->domains[rand(0, count($this->domains) - 1)]; $this->domains[random_int(0, count($this->domains) - 1)];
$keyword = $keyword =
$this->keywords[ $this->keywords[
rand(0, count($this->keywords) - 1) random_int(0, count($this->keywords) - 1)
]; ];
$browser = $browser =
$this->browsers[ $this->browsers[
rand(0, count($this->browsers) - 1) random_int(0, count($this->browsers) - 1)
]; ];
$hits = rand(0, $probability2); $hits = random_int(0, $probability2);
$websiteByBrowser[] = [ $websiteByBrowser[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,

View File

@ -483,7 +483,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid; $this->attributes['guid'] = $guid ?? $this->getLink();
return $this; return $this;
} }

View File

@ -15,7 +15,6 @@ use App\Models\ActorModel;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
@ -32,6 +31,8 @@ use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel; use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel; use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException; use RuntimeException;
@ -528,7 +529,7 @@ class Podcast extends Entity
} }
if ($this->podcasting_platforms === null) { if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting'); $this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
} }
return $this->podcasting_platforms; return $this->podcasting_platforms;
@ -546,7 +547,7 @@ class Podcast extends Entity
} }
if ($this->social_platforms === null) { if ($this->social_platforms === null) {
$this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social'); $this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
} }
return $this->social_platforms; return $this->social_platforms;
@ -564,7 +565,7 @@ class Podcast extends Entity
} }
if ($this->funding_platforms === null) { if ($this->funding_platforms === null) {
$this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding'); $this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
} }
return $this->funding_platforms; return $this->funding_platforms;

View File

@ -42,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) {
// populate data array // populate data array
$TagData = [ $TagData = [
'title' => [esc($episode->title)], 'title' => [esc($episode->title)],
'artist' => [ 'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'album' => [esc($episode->podcast->title)], 'album' => [esc($episode->podcast->title)],
'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''], 'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'], 'genre' => ['Podcast'],
'comment' => [$episode->description], 'comment' => [$episode->description],
'track_number' => [(string) $episode->number], 'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright], 'copyright_message' => [$episode->podcast->copyright],
'publisher' => [ 'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null 'encoded_by' => ['Castopod'],
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url], // 'website' => [$podcast_url],

View File

@ -164,7 +164,7 @@ if (! function_exists('parse_size')) {
$size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size. $size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size.
if ($unit !== '') { if ($unit !== '') {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(1024, (float) stripos('bkmgtpezy', $unit[0]))); return round($size * 1024 ** ((float) stripos('bkmgtpezy', $unit[0])));
} }
return round($size); return round($size);
@ -183,7 +183,7 @@ if (! function_exists('format_bytes')) {
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000)); $pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1); $pow = min($pow, count($units) - 1);
$bytes /= pow($is_binary ? 1024 : 1000, $pow); $bytes /= ($is_binary ? 1024 : 1000) ** $pow;
return round($bytes, $precision) . $units[$pow]; return round($bytes, $precision) . $units[$pow];
} }

View File

@ -260,12 +260,7 @@ if (! function_exists('get_rss_feed')) {
$itunesNamespace, $itunesNamespace,
); );
$channel->addChild( $channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, $itunesNamespace, false);
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunesNamespace,
false
);
$channel->addChild('link', $podcast->link); $channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunesNamespace); $owner = $channel->addChild('owner', null, $itunesNamespace);
@ -354,7 +349,7 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('episodeType', $episode->type, $itunesNamespace); $item->addChild('episodeType', $episode->type, $itunesNamespace);
// If episode is of type trailer, add podcast:trailer tag on channel level // If episode is of type trailer, add podcast:trailer tag on channel level
if ($episode->type == 'trailer') { if ($episode->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace); $trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace);
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822)); $trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
$trailer->addAttribute( $trailer->addAttribute(

View File

@ -48,7 +48,7 @@ class Router extends CodeIgniterRouter
$matchedKey = $routeKey; $matchedKey = $routeKey;
// Are we dealing with a locale? // Are we dealing with a locale?
if (strpos($routeKey, '{locale}') !== false) { if (str_contains($routeKey, '{locale}')) {
$routeKey = str_replace('{locale}', '[^/]+', $routeKey); $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
} }
@ -73,7 +73,7 @@ class Router extends CodeIgniterRouter
// Store our locale so CodeIgniter object can // Store our locale so CodeIgniter object can
// assign it to the Request. // assign it to the Request.
if (strpos($matchedKey, '{locale}') !== false) { if (str_contains($matchedKey, '{locale}')) {
preg_match( preg_match(
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u', '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
$uri, $uri,
@ -183,13 +183,13 @@ class Router extends CodeIgniterRouter
[$controller] = explode('::', (string) $handler); [$controller] = explode('::', (string) $handler);
// Checks `/` in controller name // Checks `/` in controller name
if (strpos($controller, '/') !== false) { if (str_contains($controller, '/')) {
throw RouterException::forInvalidControllerName($handler); throw RouterException::forInvalidControllerName($handler);
} }
if (strpos((string) $handler, '$') !== false && strpos($routeKey, '(') !== false) { if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
// Checks dynamic controller // Checks dynamic controller
if (strpos($controller, '$') !== false) { if (str_contains($controller, '$')) {
throw RouterException::forDynamicController($handler); throw RouterException::forDynamicController($handler);
} }

View File

@ -7,8 +7,6 @@ namespace ViewComponents;
use CodeIgniter\View\ViewDecoratorInterface; use CodeIgniter\View\ViewDecoratorInterface;
/** /**
* Class Decorator
*
* Enables rendering of View Components into the views. * Enables rendering of View Components into the views.
* *
* Borrowed and adapted from https://github.com/lonnieezell/Bonfire2/ * Borrowed and adapted from https://github.com/lonnieezell/Bonfire2/

View File

@ -19,7 +19,7 @@ class Theme
protected static $defaultTheme = 'app'; protected static $defaultTheme = 'app';
/** /**
* @var string * @var ?string
*/ */
protected static $currentTheme; protected static $currentTheme;
@ -71,9 +71,7 @@ class Theme
*/ */
public static function current(): string public static function current(): string
{ {
return static::$currentTheme !== null return static::$currentTheme ?? static::$defaultTheme;
? static::$currentTheme
: static::$defaultTheme;
} }
/** /**

View File

@ -272,13 +272,7 @@ class EpisodeModel extends UuidModel
$secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode($podcastId); $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode($podcastId);
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $found; return $found;

View File

@ -1,205 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PlatformModel Model for platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use App\Entities\Platform;
use CodeIgniter\Model;
use Config\App;
class PlatformModel extends Model
{
/**
* @var string
*/
protected $table = 'platforms';
/**
* @var string
*/
protected $primaryKey = 'slug';
/**
* @var list<string>
*/
protected $allowedFields = ['slug', 'type', 'label', 'home_url', 'submit_url'];
/**
* @var string
*/
protected $returnType = Platform::class;
/**
* @var bool
*/
protected $useSoftDeletes = false;
/**
* @var bool
*/
protected $useTimestamps = false;
/**
* @return Platform[]
*/
public function getPlatforms(): array
{
if (! ($found = cache('platforms'))) {
$baseUrl = rtrim(config(App::class)->baseURL, '/');
$found = $this->select(
"*, CONCAT('{$baseUrl}/assets/images/platforms/',`type`,'/',`slug`,'.svg') as icon",
)->findAll();
cache()
->save('platforms', $found, DECADE);
}
return $found;
}
public function getPlatform(string $slug): ?Platform
{
$cacheName = "platform-{$slug}";
if (! ($found = cache($cacheName))) {
$found = $this->where('slug', $slug)
->first();
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
public function createPlatform(
string $slug,
string $type,
string $label,
string $homeUrl,
string $submitUrl = null
): bool {
$data = [
'slug' => $slug,
'type' => $type,
'label' => $label,
'home_url' => $homeUrl,
'submit_url' => $submitUrl,
];
return $this->insert($data, false);
}
/**
* @return Platform[]
*/
public function getPlatformsWithLinks(int $podcastId, string $platformType): array
{
if (
! ($found = cache("podcast#{$podcastId}_platforms_{$platformType}_withLinks"))
) {
$found = $this->select(
'platforms.*, podcasts_platforms.link_url, podcasts_platforms.account_id, podcasts_platforms.is_visible, podcasts_platforms.is_on_embed',
)
->join(
'podcasts_platforms',
"podcasts_platforms.platform_slug = platforms.slug AND podcasts_platforms.podcast_id = {$podcastId}",
'left',
)
->where('platforms.type', $platformType)
->findAll();
cache()
->save("podcast#{$podcastId}_platforms_{$platformType}_withLinks", $found, DECADE);
}
return $found;
}
/**
* @return Platform[]
*/
public function getPodcastPlatforms(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
if (! ($found = cache($cacheName))) {
$found = $this->select(
'platforms.*, podcasts_platforms.link_url, podcasts_platforms.account_id, podcasts_platforms.is_visible, podcasts_platforms.is_on_embed',
)
->join('podcasts_platforms', 'podcasts_platforms.platform_slug = platforms.slug')
->where('podcasts_platforms.podcast_id', $podcastId)
->where('platforms.type', $platformType)
->findAll();
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @param mixed[] $podcastsPlatformsData
*
* @return int|false Number of rows inserted or FALSE on failure
*/
public function savePodcastPlatforms(
int $podcastId,
string $platformType,
array $podcastsPlatformsData
): int | false {
$this->clearCache($podcastId);
$podcastsPlatformsTable = $this->db->prefixTable('podcasts_platforms');
$platformsTable = $this->db->prefixTable('platforms');
$deleteJoinQuery = <<<SQL
DELETE {$podcastsPlatformsTable}
FROM {$podcastsPlatformsTable}
INNER JOIN {$platformsTable} ON {$platformsTable}.slug = {$podcastsPlatformsTable}.platform_slug
WHERE `podcast_id` = ? AND `type` = ?
SQL;
$this->db->query($deleteJoinQuery, [$podcastId, $platformType]);
if ($podcastsPlatformsData === []) {
// no rows inserted
return 0;
}
return $this->db
->table('podcasts_platforms')
->insertBatch($podcastsPlatformsData);
}
public function removePodcastPlatform(int $podcastId, string $platformSlug): bool | string
{
$this->clearCache($podcastId);
return $this->db->table('podcasts_platforms')
->delete([
'podcast_id' => $podcastId,
'platform_slug' => $platformSlug,
]);
}
public function clearCache(int $podcastId): void
{
cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
// delete localized podcast page cache
cache()
->deleteMatching("page_podcast#{$podcastId}*");
// delete post and episode comments pages cache
cache()
->deleteMatching('page_post*');
cache()
->deleteMatching('page_episode#*');
}
}

View File

@ -259,13 +259,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId); $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId);
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $found; return $found;
@ -295,13 +289,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId); $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId);
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $found; return $found;
@ -335,11 +323,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId); $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId);
cache() cache()
->save( ->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$defaultQuery,
$secondsToNextUnpublishedEpisode ? $secondsToNextUnpublishedEpisode : DECADE
);
} }
return $defaultQuery; return $defaultQuery;

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0,0H24V24H0Z" fill="none" />
<path d="M13.3,13.07a1.91,1.91,0,0,1,.48,0,2.51,2.51,0,0,1,1.88,1.63c.08.26.19.66.26.93l.09.37h.38a5.49,5.49,0,0,0,5.38-5.46c0-4.52-3.77-8.28-9.54-8.28A10.16,10.16,0,0,0,5.9,4.56,10,10,0,0,0,2.23,12.3a10.64,10.64,0,0,0,.28,2.35,10,10,0,0,0,9.72,7.65,11.07,11.07,0,0,0,1.3-.08A10,10,0,0,0,20,18.6l.1-.12-.2-.73-.38.1a14.32,14.32,0,0,1-3.72.48,14.14,14.14,0,0,1-3-.31,2.51,2.51,0,0,1-1.87-1.65,2.51,2.51,0,0,1,.48-2.44A2.5,2.5,0,0,1,13.3,13.07ZM12.23,3.24c5.34,0,8.6,3.42,8.6,7.34a4.55,4.55,0,0,1-4.1,4.5c-.06-.22-.13-.45-.18-.62a15.25,15.25,0,0,0-9.36-9.7A8.79,8.79,0,0,1,12.23,3.24ZM3.17,12.3A9,9,0,0,1,6.3,5.45a14.35,14.35,0,0,1,8.38,7A3.26,3.26,0,0,0,14,12.2a15.86,15.86,0,0,0-3.17-.33,15.65,15.65,0,0,0-7.51,1.94A9.33,9.33,0,0,1,3.17,12.3Zm9.44,6.64a15.9,15.9,0,0,0,3.19.33,16.77,16.77,0,0,0,2.46-.19,9,9,0,0,1-4.6,2.17,13.91,13.91,0,0,1-1.73-2.52A3.83,3.83,0,0,0,12.61,18.94Zm-.07,2.42h-.31a8.82,8.82,0,0,1-4.16-1A14.24,14.24,0,0,1,9.88,16a4.07,4.07,0,0,0,.16.71A15.68,15.68,0,0,0,12.54,21.36Zm-1.83-8a15.6,15.6,0,0,0-3.48,6.55,9,9,0,0,1-3.72-5.1,14.27,14.27,0,0,1,7.29-1.95h.44A3.57,3.57,0,0,0,10.71,13.31Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0,0H24V24H0Z" fill="none" />
<path d="M3.22,10.08A1.24,1.24,0,0,0,2,11.31V12.7a1.22,1.22,0,0,0,2.44,0h0V11.31A1.17,1.17,0,0,0,3.22,10.08Zm17.56,0a1.23,1.23,0,0,0-1.22,1.23V12.7a1.21,1.21,0,0,0,1.22,1.22A1.22,1.22,0,0,0,22,12.7h0V11.31A1.24,1.24,0,0,0,20.78,10.08ZM7.56,14.2a1.24,1.24,0,0,0-1.23,1.23v1.4a1.23,1.23,0,0,0,2.45,0h0v-1.4A1.17,1.17,0,0,0,7.56,14.2ZM7.56,6A1.24,1.24,0,0,0,6.33,7.24V11.7h0a1.23,1.23,0,0,0,2.45,0h0V7.24A1.17,1.17,0,0,0,7.56,6Zm8.88,0a1.24,1.24,0,0,0-1.22,1.23V8.63a1.23,1.23,0,0,0,2.45,0h0V7.24A1.24,1.24,0,0,0,16.44,6ZM12,2a1.23,1.23,0,0,0-1.22,1.23V4.62a1.22,1.22,0,1,0,2.44,0h0V3.23A1.23,1.23,0,0,0,12,2Zm0,16.16a1.22,1.22,0,0,0-1.22,1.22v1.4a1.22,1.22,0,1,0,2.44,0h0v-1.4A1.26,1.26,0,0,0,12,18.16Zm4.44-7a1.23,1.23,0,0,0-1.22,1.22v4.47a1.23,1.23,0,0,0,2.45,0h0V12.36A1.23,1.23,0,0,0,16.44,11.14ZM13.22,8.41a1.22,1.22,0,0,0-2.44,0h0v7.36h0a1.22,1.22,0,1,0,2.44,0h0V8.41Z" />
</svg>

Before

Width:  |  Height:  |  Size: 991 B

View File

@ -42,7 +42,7 @@ class Alert extends Component
$this->variant = 'default'; $this->variant = 'default';
} }
$glyph = icon(($this->glyph === null ? $variants[$this->variant]['glyph'] : $this->glyph), 'flex-shrink-0 mr-2 text-lg'); $glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), 'flex-shrink-0 mr-2 text-lg');
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>'; $title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class; $class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;

View File

@ -28,17 +28,18 @@ class Button extends Component
public function render(): string public function render(): string
{ {
$baseClass = $baseClass =
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent'; 'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
$variantClass = [ $variantClass = [
'default' => 'text-black bg-gray-300 hover:bg-gray-400', 'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
'primary' => 'text-accent-contrast bg-accent-base hover:bg-accent-hover', 'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
'secondary' => 'border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover', 'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'success' => 'text-white bg-pine-500 hover:bg-pine-800', 'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
'danger' => 'text-white bg-red-600 hover:bg-red-700', 'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600', 'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
'info' => 'text-white bg-blue-500 hover:bg-blue-600', 'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
'disabled' => 'text-black bg-gray-300 cursor-not-allowed', 'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
'link' => 'text-accent-base bg-transparent underline hover:no-underline',
]; ];
$sizeClass = [ $sizeClass = [

2
builds
View File

@ -1,6 +1,8 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
declare(strict_types=1);
define('LATEST_RELEASE', '^4.0'); define('LATEST_RELEASE', '^4.0');
define('GITHUB_URL', 'https://github.com/codeigniter4/codeigniter4'); define('GITHUB_URL', 'https://github.com/codeigniter4/codeigniter4');

View File

@ -9,7 +9,7 @@
"php": "^8.1", "php": "^8.1",
"adaures/ipcat-php": "^v1.0.0", "adaures/ipcat-php": "^v1.0.0",
"adaures/podcast-persons-taxonomy": "^v1.0.1", "adaures/podcast-persons-taxonomy": "^v1.0.1",
"aws/aws-sdk-php": "^3.305.0", "aws/aws-sdk-php": "^3.305.1",
"chrisjean/php-ico": "^1.0.4", "chrisjean/php-ico": "^1.0.4",
"cocur/slugify": "^v4.5.1", "cocur/slugify": "^v4.5.1",
"codeigniter4/framework": "v4.5.1", "codeigniter4/framework": "v4.5.1",
@ -35,10 +35,10 @@
"mikey179/vfsstream": "^v1.6.11", "mikey179/vfsstream": "^v1.6.11",
"phpstan/extension-installer": "^1.3.1", "phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.10.67", "phpstan/phpstan": "^1.10.67",
"phpunit/phpunit": "^10.5.19", "phpunit/phpunit": "^10.5.20",
"rector/rector": "^1.0.4", "rector/rector": "^1.0.4",
"symplify/coding-standard": "^12.0.7", "symplify/coding-standard": "^12.1.4",
"symplify/easy-coding-standard": "^12.0.13" "symplify/easy-coding-standard": "^12.1.14"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

1352
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -303,11 +303,8 @@ You may skip this section if you go through the install wizard (go to
# Populates all Languages # Populates all Languages
php spark db:seed LanguageSeeder php spark db:seed LanguageSeeder
# Populates all podcasts platforms
php spark db:seed PlatformSeeder
# Adds a superadmin with [admin@castopod.local / castopod] credentials # Adds a superadmin with [admin@castopod.local / castopod] credentials
php spark db:seed PlatformSeeder php spark db:seed DevSuperadminSeeder
``` ```
3. (optionnal) Populate the database with test data: 3. (optionnal) Populate the database with test data:

30
ecs.php
View File

@ -10,26 +10,20 @@ use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;
use Symplify\CodingStandard\Fixer\Naming\StandardizeHereNowDocKeywordFixer; use Symplify\CodingStandard\Fixer\Naming\StandardizeHereNowDocKeywordFixer;
use Symplify\CodingStandard\Fixer\Spacing\MethodChainingNewlineFixer; use Symplify\CodingStandard\Fixer\Spacing\MethodChainingNewlineFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig; use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $ecsConfig): void { return ECSConfig::configure()
// alternative to CLI arguments, easier to maintain and extend ->withPaths([
$ecsConfig->paths([
__DIR__ . '/app', __DIR__ . '/app',
__DIR__ . '/modules', __DIR__ . '/modules',
__DIR__ . '/themes', __DIR__ . '/themes',
__DIR__ . '/tests', __DIR__ . '/tests',
__DIR__ . '/public', __DIR__ . '/public',
__DIR__ . '/builds', __DIR__ . '/builds',
__DIR__ . '/ecs.php',
__DIR__ . '/preload.php',
__DIR__ . '/rector.php',
__DIR__ . '/spark', __DIR__ . '/spark',
]); ])
->withRootFiles()
$ecsConfig->sets([SetList::CLEAN_CODE, SetList::COMMON, SetList::SYMPLIFY, SetList::PSR_12]); ->withPreparedSets(cleanCode: true, common: true, symplify: true, strict: true, psr12: true)
->withSkip([
$ecsConfig->skip([
// skip specific generated files // skip specific generated files
__DIR__ . '/modules/Admin/Language/*/PersonsTaxonomy.php', __DIR__ . '/modules/Admin/Language/*/PersonsTaxonomy.php',
@ -40,11 +34,7 @@ return static function (ECSConfig $ecsConfig): void {
__DIR__ . '/app/Helpers/components_helper.php', __DIR__ . '/app/Helpers/components_helper.php',
], ],
LineLengthFixer::class => [ LineLengthFixer::class => [__DIR__ . '/app/Views/*', __DIR__ . '/modules/**/Views/*', __DIR__ . '/themes/*'],
__DIR__ . '/app/Views/*',
__DIR__ . '/modules/**/Views/*',
__DIR__ . '/themes/*',
],
IndentationTypeFixer::class => [ IndentationTypeFixer::class => [
__DIR__ . '/app/Views/*', __DIR__ . '/app/Views/*',
@ -65,11 +55,9 @@ return static function (ECSConfig $ecsConfig): void {
BinaryOperatorSpacesFixer::class => [__DIR__ . '/app/Language/*', __DIR__ . '/modules/**/Language/*'], BinaryOperatorSpacesFixer::class => [__DIR__ . '/app/Language/*', __DIR__ . '/modules/**/Language/*'],
AssignmentInConditionSniff::class, AssignmentInConditionSniff::class,
]); ])
->withConfiguredRule(BinaryOperatorSpacesFixer::class, [
$ecsConfig->ruleWithConfiguration(BinaryOperatorSpacesFixer::class, [
'operators' => [ 'operators' => [
'=>' => 'align_single_space_minimal', '=>' => 'align_single_space_minimal',
], ],
]); ]);
};

View File

@ -511,48 +511,6 @@ $routes->group(
}); });
}); });
}); });
$routes->group('platforms', static function ($routes): void {
$routes->get(
'/',
'PodcastPlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'social',
'PodcastPlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'funding',
'PodcastPlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->post(
'save/(:platformType)',
'PodcastPlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'(:slug)/podcast-platform-remove',
'PodcastPlatformController::removePodcastPlatform/$1/$2',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast#.manage-platforms',
],
);
});
// Podcast notifications // Podcast notifications
$routes->group('notifications', static function ($routes): void { $routes->group('notifications', static function ($routes): void {
$routes->get('/', 'NotificationController::list/$1', [ $routes->get('/', 'NotificationController::list/$1', [

View File

@ -12,8 +12,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all * BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. Extend this class in any new controllers: class Home extends BaseController * your controllers. Extend this class in any new controllers: class Home extends BaseController
* *

View File

@ -326,12 +326,8 @@ class EpisodeController extends BaseController
$this->request->getPost('parental_advisory') !== 'undefined' $this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory') ? $this->request->getPost('parental_advisory')
: null; : null;
$this->episode->number = $this->request->getPost('episode_number') $this->episode->number = $this->request->getPost('episode_number') ?: null;
? $this->request->getPost('episode_number') $this->episode->season_number = $this->request->getPost('season_number') ?: null;
: null;
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null;
$this->episode->type = $this->request->getPost('type'); $this->episode->type = $this->request->getPost('type');
$this->episode->is_blocked = $this->request->getPost('block') === 'yes'; $this->episode->is_blocked = $this->request->getPost('block') === 'yes';
$this->episode->custom_rss_string = $this->request->getPost('custom_rss'); $this->episode->custom_rss_string = $this->request->getPost('custom_rss');

View File

@ -17,7 +17,7 @@ use CodeIgniter\HTTP\RedirectResponse;
class PageController extends BaseController class PageController extends BaseController
{ {
protected ?Page $page; protected ?Page $page = null;
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {

View File

@ -18,7 +18,7 @@ use Modules\Media\Models\MediaModel;
class PersonController extends BaseController class PersonController extends BaseController
{ {
protected ?Person $person; protected ?Person $person = null;
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {

View File

@ -30,9 +30,7 @@ trait AnalyticsTrait
$referer = $session->get('referer'); $referer = $session->get('referer');
$domain = $domain =
parse_url((string) $referer, PHP_URL_HOST) === null parse_url((string) $referer, PHP_URL_HOST) ?? '- Direct -';
? '- Direct -'
: parse_url((string) $referer, PHP_URL_HOST);
parse_str((string) parse_url((string) $referer, PHP_URL_QUERY), $queries); parse_str((string) parse_url((string) $referer, PHP_URL_QUERY), $queries);
$keywords = $queries['q'] ?? null; $keywords = $queries['q'] ?? null;

View File

@ -2,7 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use CodeIgniter\Router\RouteCollection;
use Modules\Analytics\Config\Analytics; use Modules\Analytics\Config\Analytics;
/** /**

View File

@ -85,14 +85,10 @@ if (! function_exists('set_user_session_location')) {
$city = $cityReader->city(client_ip()); $city = $cityReader->city(client_ip());
$location = [ $location = [
'countryCode' => $city->country->isoCode === null 'countryCode' => $city->country->isoCode ?? 'N/A',
? 'N/A' 'regionCode' => $city->subdivisions[0]->isoCode ?? 'N/A',
: $city->country->isoCode, 'latitude' => round($city->location->latitude, 3),
'regionCode' => $city->subdivisions[0]->isoCode === null 'longitude' => round($city->location->longitude, 3),
? 'N/A'
: $city->subdivisions[0]->isoCode,
'latitude' => round($city->location->latitude, 3),
'longitude' => round($city->location->longitude, 3),
]; ];
// If things go wrong the show must go on and the user must be able to download the file // If things go wrong the show must go on and the user must be able to download the file
} catch (Exception) { } catch (Exception) {
@ -179,9 +175,7 @@ if (! function_exists('set_user_session_referer')) {
{ {
$session = Services::session(); $session = Services::session();
$newreferer = isset($_SERVER['HTTP_REFERER']) $newreferer = $_SERVER['HTTP_REFERER'] ?? '- Direct -';
? $_SERVER['HTTP_REFERER']
: '- Direct -';
$newreferer = $newreferer =
parse_url((string) $newreferer, PHP_URL_HOST) === parse_url((string) $newreferer, PHP_URL_HOST) ===
parse_url(current_url(false), PHP_URL_HOST) parse_url(current_url(false), PHP_URL_HOST)
@ -250,9 +244,7 @@ if (! function_exists('podcast_hit')) {
} }
//We get the HTTP header field `Range`: //We get the HTTP header field `Range`:
$httpRange = isset($_SERVER['HTTP_RANGE']) $httpRange = $_SERVER['HTTP_RANGE'] ?? null;
? $_SERVER['HTTP_RANGE']
: null;
$salt = config(Analytics::class) $salt = config(Analytics::class)
->salt; ->salt;

View File

@ -65,7 +65,7 @@ class EpisodeController extends Controller
return $this->failNotFound('Episode not found'); return $this->failNotFound('Episode not found');
} }
return $this->respond($this->mapEpisode($episode)); return $this->respond(static::mapEpisode($episode));
} }
protected static function mapEpisode(Episode $episode): Episode protected static function mapEpisode(Episode $episode): Episode

View File

@ -36,7 +36,7 @@ class ApiFilter implements FilterInterface
} }
$authHeader = $request->getHeaderLine('Authorization'); $authHeader = $request->getHeaderLine('Authorization');
if (substr($authHeader, 0, 6) !== 'Basic ') { if (! str_starts_with($authHeader, 'Basic ')) {
$response->setStatusCode(401); $response->setStatusCode(401);
return $response; return $response;
@ -44,7 +44,7 @@ class ApiFilter implements FilterInterface
$auth_token = base64_decode(substr($authHeader, 6), true); $auth_token = base64_decode(substr($authHeader, 6), true);
list($username, $password) = explode(':', (string) $auth_token); [$username, $password] = explode(':', (string) $auth_token);
if (! ($username === $restApiConfig->basicAuthUsername && $password === $restApiConfig->basicAuthPassword)) { if (! ($username === $restApiConfig->basicAuthUsername && $password === $restApiConfig->basicAuthPassword)) {
$response->setStatusCode(401); $response->setStatusCode(401);

View File

@ -11,8 +11,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class ActionController
*
* A generic controller to handle Authentication Actions. * A generic controller to handle Authentication Actions.
*/ */
class ActionController extends ShieldActionController class ActionController extends ShieldActionController

View File

@ -22,7 +22,7 @@ class ContributorController extends BaseController
{ {
protected Podcast $podcast; protected Podcast $podcast;
protected ?User $contributor; protected ?User $contributor = null;
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {

View File

@ -8,8 +8,6 @@ use CodeIgniter\Controller;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
/** /**
* Class ActionController
*
* A generic controller to handle Authentication Actions. * A generic controller to handle Authentication Actions.
*/ */
class InteractController extends Controller class InteractController extends Controller

View File

@ -11,8 +11,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class RegisterController
*
* Handles displaying registration form, and handling actual registration flow. * Handles displaying registration form, and handling actual registration flow.
*/ */
class RegisterController extends ShieldRegisterController class RegisterController extends ShieldRegisterController

View File

@ -22,7 +22,7 @@ use Modules\Auth\Models\UserModel;
class UserController extends BaseController class UserController extends BaseController
{ {
protected ?User $user; protected ?User $user = null;
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {

View File

@ -102,9 +102,10 @@ if (! function_exists('add_podcast_group')) {
if (! function_exists('get_instance_group')) { if (! function_exists('get_instance_group')) {
function get_instance_group(User $user): ?string function get_instance_group(User $user): ?string
{ {
$instanceGroups = array_filter($user->getGroups() ?? [], static function ($group): bool { $instanceGroups = array_filter(
return ! str_starts_with($group, 'podcast#'); $user->getGroups() ?? [],
}); static fn ($group): bool => ! str_starts_with((string) $group, 'podcast#')
);
if ($instanceGroups === []) { if ($instanceGroups === []) {
return null; return null;
@ -138,9 +139,10 @@ if (! function_exists('set_instance_group')) {
if (! function_exists('get_podcast_group')) { if (! function_exists('get_podcast_group')) {
function get_podcast_group(User $user, int $podcastId, bool $removePrefix = true): ?string function get_podcast_group(User $user, int $podcastId, bool $removePrefix = true): ?string
{ {
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group) use ($podcastId): bool { $podcastGroups = array_filter(
return str_starts_with($group, "podcast#{$podcastId}-"); $user->getGroups() ?? [],
}); static fn ($group): bool => str_starts_with((string) $group, "podcast#{$podcastId}-")
);
if ($podcastGroups === []) { if ($podcastGroups === []) {
return null; return null;
@ -180,9 +182,10 @@ if (! function_exists('get_podcast_groups')) {
*/ */
function get_user_podcast_ids(User $user): array function get_user_podcast_ids(User $user): array
{ {
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group): bool { $podcastGroups = array_filter(
return str_starts_with($group, 'podcast#'); $user->getGroups() ?? [],
}); static fn ($group): bool => str_starts_with((string) $group, 'podcast#')
);
$userPodcastIds = []; $userPodcastIds = [];
// extract all podcast ids from groups // extract all podcast ids from groups

View File

@ -42,9 +42,7 @@ abstract class AbstractObject
} }
// removes all NULL, FALSE and Empty Strings but leaves 0 (zero) values // removes all NULL, FALSE and Empty Strings but leaves 0 (zero) values
return array_filter($array, static function ($value): bool { return array_filter($array, static fn ($value): bool => $value !== null && $value !== false && $value !== '');
return $value !== null && $value !== false && $value !== '';
});
} }
public function toJSON(): string public function toJSON(): string

View File

@ -175,12 +175,10 @@ if (! function_exists('create_preview_card_from_url')) {
// Check that, at least, the url and title are set // Check that, at least, the url and title are set
$newPreviewCard = new PreviewCard([ $newPreviewCard = new PreviewCard([
'url' => $mediaUrl, 'url' => $mediaUrl,
'title' => $media['title'] ?? '', 'title' => $media['title'] ?? '',
'description' => $media['description'] ?? '', 'description' => $media['description'] ?? '',
'type' => isset($typeMapping[$media['type']]) 'type' => $typeMapping[$media['type']] ?? 'link',
? $typeMapping[$media['type']]
: 'link',
'author_name' => $media['author_name'] ?? null, 'author_name' => $media['author_name'] ?? null,
'author_url' => $media['author_url'] ?? null, 'author_url' => $media['author_url'] ?? null,
'provider_name' => $media['provider_name'] ?? '', 'provider_name' => $media['provider_name'] ?? '',

View File

@ -134,7 +134,7 @@ class PostModel extends UuidModel
$secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId); $secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId);
cache() cache()
->save($cacheName, $found, $secondsToNextUnpublishedPost ? $secondsToNextUnpublishedPost : DECADE); ->save($cacheName, $found, $secondsToNextUnpublishedPost ?: DECADE);
} }
return $found; return $found;

View File

@ -160,9 +160,7 @@ class InstallController extends Controller
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
return redirect() return redirect()
->to( ->to((host_url() ?? config(App::class) ->baseURL) . config(Install::class)->gateway)
(host_url() === null ? config(App::class) ->baseURL : host_url()) . config(Install::class)->gateway
)
->withInput() ->withInput()
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }

View File

@ -85,16 +85,12 @@ class Transcript extends BaseMedia
} }
$transcript_format = $this->file->getExtension(); $transcript_format = $this->file->getExtension();
switch ($transcript_format) { $transcriptJson = match ($transcript_format) {
case 'vtt': 'vtt' => $transcriptParser->loadString($transcriptContent)
$transcriptJson = $transcriptParser->loadString($transcriptContent) ->parseVtt(),
->parseVtt(); default => $transcriptParser->loadString($transcriptContent)
break; ->parseSrt(),
case 'srt': };
default:
$transcriptJson = $transcriptParser->loadString($transcriptContent)
->parseSrt();
}
$tempFilePath = WRITEPATH . 'uploads/' . $this->file->getRandomName(); $tempFilePath = WRITEPATH . 'uploads/' . $this->file->getRandomName();
file_put_contents($tempFilePath, $transcriptJson); file_put_contents($tempFilePath, $transcriptJson);

View File

@ -543,9 +543,9 @@ class VideoClipper
# find unique color # find unique color
do { do {
$r = rand(0, 255); $r = random_int(0, 255);
$g = rand(0, 255); $g = random_int(0, 255);
$b = rand(0, 255); $b = random_int(0, 255);
} while (imagecolorexact($src, $r, $g, $b) < 0); } while (imagecolorexact($src, $r, $g, $b) < 0);
$ns = $s * $q; $ns = $s * $q;

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Modules\PremiumPodcasts\Config;
use CodeIgniter\Router\RouteCollection;
use Modules\Admin\Config\Admin;
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
/** @var RouteCollection $routes */
// Admin routes for subscriptions
$routes->group(
config(Admin::class)
->gateway,
[
'namespace' => 'Modules\Platforms\Controllers',
],
static function ($routes): void {
$routes->group('podcasts/(:num)/platforms', static function ($routes): void {
$routes->get(
'/',
'PlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'social',
'PlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'funding',
'PlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->post(
'save/(:platformType)',
'PlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'(:platformType)/(:slug)/podcast-platform-remove',
'PlatformController::removePlatform/$1/$2/$3',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast#.manage-platforms',
],
);
});
}
);

View File

@ -8,18 +8,19 @@ declare(strict_types=1);
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace Modules\Admin\Controllers; namespace Modules\Platforms\Controllers;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Models\PlatformModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use Config\Services; use Config\Services;
use Modules\Admin\Controllers\BaseController;
use Modules\Platforms\Models\PlatformModel;
class PodcastPlatformController extends BaseController class PlatformController extends BaseController
{ {
protected ?Podcast $podcast; protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
@ -28,18 +29,20 @@ class PodcastPlatformController extends BaseController
} }
if ( if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) instanceof Podcast ! ($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) instanceof Podcast
) { ) {
unset($params[0]); throw PageNotFoundException::forPageNotFound();
return $this->{$method}(...$params);
} }
throw PageNotFoundException::forPageNotFound(); $this->podcast = $podcast;
unset($params[0]);
return $this->{$method}(...$params);
} }
public function index(): string public function index(): string
{ {
return view('podcast/platforms\dashboard'); return view('podcast/platforms/dashboard');
} }
public function platforms(string $platformType): string public function platforms(string $platformType): string
@ -49,7 +52,7 @@ class PodcastPlatformController extends BaseController
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'platformType' => $platformType, 'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks($this->podcast->id, $platformType), 'platforms' => (new PlatformModel())->getPlatformsWithData($this->podcast->id, $platformType),
]; ];
replace_breadcrumb_params([ replace_breadcrumb_params([
@ -64,8 +67,7 @@ class PodcastPlatformController extends BaseController
$platformModel = new PlatformModel(); $platformModel = new PlatformModel();
$validation = Services::validation(); $validation = Services::validation();
$podcastsPlatformsData = []; $platformsData = [];
foreach ( foreach (
$this->request->getPost('platforms') $this->request->getPost('platforms')
as $platformSlug => $podcastPlatform as $platformSlug => $podcastPlatform
@ -80,30 +82,27 @@ class PodcastPlatformController extends BaseController
} }
$podcastPlatformAccountId = trim((string) $podcastPlatform['account_id']); $podcastPlatformAccountId = trim((string) $podcastPlatform['account_id']);
$podcastsPlatformsData[] = [ $platformsData[] = [
'platform_slug' => $platformSlug, 'podcast_id' => $this->podcast->id,
'podcast_id' => $this->podcast->id, 'type' => $platformType,
'link_url' => $podcastPlatformUrl, 'slug' => $platformSlug,
'account_id' => $podcastPlatformAccountId === '' ? null : $podcastPlatformAccountId, 'link_url' => $podcastPlatformUrl,
'is_visible' => array_key_exists('visible', $podcastPlatform) && 'account_id' => $podcastPlatformAccountId === '' ? null : $podcastPlatformAccountId,
'is_visible' => array_key_exists('visible', $podcastPlatform) &&
$podcastPlatform['visible'] === 'yes', $podcastPlatform['visible'] === 'yes',
'is_on_embed' => array_key_exists(
'on_embed',
$podcastPlatform
) && $podcastPlatform['on_embed'] === 'yes',
]; ];
} }
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType, $podcastsPlatformsData); $platformModel->savePlatforms($this->podcast->id, $platformType, $platformsData);
return redirect() return redirect()
->back() ->back()
->with('message', lang('Platforms.messages.updateSuccess')); ->with('message', lang('Platforms.messages.updateSuccess'));
} }
public function removePodcastPlatform(string $platformSlug): RedirectResponse public function removePlatform(string $platformType, string $platformSlug): RedirectResponse
{ {
(new PlatformModel())->removePodcastPlatform($this->podcast->id, $platformSlug); (new PlatformModel())->removePlatform($this->podcast->id, $platformType, $platformSlug);
return redirect() return redirect()
->back() ->back()

View File

@ -8,20 +8,20 @@ declare(strict_types=1);
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace Modules\Platforms\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
/** /**
* @property int $podcast_id
* @property string $slug * @property string $slug
* @property string $type * @property string $type
* @property string $label * @property string $label
* @property string $link_url
* @property string|null $account_id
* @property bool $is_visible
* @property string $home_url * @property string $home_url
* @property string|null $submit_url * @property string|null $submit_url
* @property string|null $link_url
* @property string|null $account_id
* @property bool|null $is_visible
* @property bool|null $is_on_embed
*/ */
class Platform extends Entity class Platform extends Entity
{ {
@ -29,14 +29,14 @@ class Platform extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'slug' => 'string', 'podcast_id' => 'int',
'type' => 'string', 'slug' => 'string',
'label' => 'string', 'type' => 'string',
'home_url' => 'string', 'label' => 'string',
'submit_url' => '?string', 'link_url' => 'string',
'link_url' => '?string', 'account_id' => '?string',
'account_id' => '?string', 'is_visible' => 'boolean',
'is_visible' => '?boolean', 'home_url' => 'string',
'is_on_embed' => '?boolean', 'submit_url' => '?string',
]; ];
} }

View File

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* Class PlatformModel Model for platforms table in database
*
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Platforms\Models;
use CodeIgniter\Model;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Platforms;
class PlatformModel extends Model
{
/**
* @var string
*/
protected $table = 'platforms';
/**
* @var string
*/
protected $primaryKey = 'id';
/**
* @var string[]
*/
protected $allowedFields = ['podcast_id', 'type', 'slug', 'link_url', 'account_id', 'is_visible'];
/**
* @var string
*/
protected $returnType = Platform::class;
/**
* @var bool
*/
protected $useSoftDeletes = false;
/**
* @var bool
*/
protected $useTimestamps = false;
/**
* @return Platform[]
*/
public function getPlatformsWithData(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}_withData";
if (! ($found = cache($cacheName))) {
$platforms = new Platforms();
$found = $this->getPlatforms($podcastId, $platformType);
$platformsData = $platforms->getPlatformsByType($platformType);
$knownSlugs = [];
foreach ($found as $podcastPlatform) {
$knownSlugs[] = $podcastPlatform->slug;
}
foreach ($platformsData as $slug => $platform) {
if (! in_array($slug, $knownSlugs, true)) {
$found[] = new Platform([
'podcast_id' => $podcastId,
'slug' => $slug,
'type' => $platformType,
'label' => $platform['label'],
'home_url' => $platform['home_url'],
'submit_url' => $platform['submit_url'],
'link_url' => '',
'account_id' => null,
'is_visible' => false,
]);
}
}
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return Platform[]
*/
public function getPlatforms(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
if (! ($found = cache($cacheName))) {
$platforms = new Platforms();
/** @var Platform[] $found */
$found = $this
->where('podcast_id', $podcastId)
->where('type', $platformType)
->orderBy('slug')
->findAll();
foreach ($found as $platform) {
$platformData = $platforms->findPlatformBySlug($platformType, $platform->slug);
if ($platformData === null) {
// delete platform, it does not correspond to any existing one
$this->delete($platform->id);
continue;
}
$platform->type = $platformType;
$platform->label = $platformData['label'];
$platform->home_url = $platformData['home_url'];
$platform->submit_url = $platformData['submit_url'];
}
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return int|false Number of rows inserted or FALSE on failure
*/
public function savePlatforms(int $podcastId, string $platformType, array $data): int | false
{
$this->clearCache($podcastId);
$platforms = new Platforms();
$platformsData = $platforms->getPlatformsByType($platformType);
$this->builder()
->whereIn('slug', array_keys($platformsData))
->delete();
if ($data === []) {
// no rows inserted
return 0;
}
return $this->insertBatch($data);
}
public function removePlatform(int $podcastId, string $platformType, string $platformSlug): bool | string
{
$this->clearCache($podcastId);
return $this->builder()
->delete([
'podcast_id' => $podcastId,
'type' => $platformType,
'slug' => $platformSlug,
]);
}
public function clearCache(int $podcastId): void
{
cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
// delete localized podcast page cache
cache()
->deleteMatching("page_podcast#{$podcastId}*");
// delete post and episode comments pages cache
cache()
->deleteMatching('page_post*');
cache()
->deleteMatching('page_episode#*');
}
}

View File

@ -8,11 +8,9 @@ use AdAures\PodcastPersonsTaxonomy\ReversedTaxonomy;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Person; use App\Entities\Person;
use App\Entities\Platform;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\CLI;
@ -23,6 +21,8 @@ use Exception;
use League\HTMLToMarkdown\HtmlConverter; use League\HTMLToMarkdown\HtmlConverter;
use Modules\Auth\Config\AuthGroups; use Modules\Auth\Config\AuthGroups;
use Modules\Auth\Models\UserModel; use Modules\Auth\Models\UserModel;
use Modules\Platforms\Models\PlatformModel;
use Modules\Platforms\Platforms;
use Modules\PodcastImport\Entities\PodcastImportTask; use Modules\PodcastImport\Entities\PodcastImportTask;
use Modules\PodcastImport\Entities\TaskStatus; use Modules\PodcastImport\Entities\TaskStatus;
use PodcastFeed\PodcastFeed; use PodcastFeed\PodcastFeed;
@ -52,9 +52,9 @@ class PodcastImport extends BaseCommand
$importQueue = get_import_tasks(); $importQueue = get_import_tasks();
$currentImport = current(array_filter($importQueue, static function ($task): bool { $currentImport = current(
return $task->status === TaskStatus::Running; array_filter($importQueue, static fn ($task): bool => $task->status === TaskStatus::Running)
})); );
if ($currentImport instanceof PodcastImportTask) { if ($currentImport instanceof PodcastImportTask) {
$currentImport->syncWithProcess(); $currentImport->syncWithProcess();
@ -68,9 +68,7 @@ class PodcastImport extends BaseCommand
} }
// Get the next queued import // Get the next queued import
$queuedImports = array_filter($importQueue, static function ($task): bool { $queuedImports = array_filter($importQueue, static fn ($task): bool => $task->status === TaskStatus::Queued);
return $task->status === TaskStatus::Queued;
});
$nextImport = end($queuedImports); $nextImport = end($queuedImports);
if (! $nextImport instanceof PodcastImportTask) { if (! $nextImport instanceof PodcastImportTask) {
@ -392,27 +390,32 @@ class PodcastImport extends BaseCommand
], ],
]; ];
$platforms = new Platforms();
$platformModel = new PlatformModel(); $platformModel = new PlatformModel();
foreach ($platformTypes as $platformType) { foreach ($platformTypes as $platformType) {
$podcastsPlatformsData = []; $platformsData = [];
$currPlatformStep = 1; // for progress $currPlatformStep = 1; // for progress
CLI::write($platformType['name'] . ' - ' . $platformType['count'] . ' elements'); CLI::write($platformType['name'] . ' - ' . $platformType['count'] . ' elements');
foreach ($platformType['elements'] as $platform) { foreach ($platformType['elements'] as $platform) {
CLI::showProgress($currPlatformStep++, $platformType['count']); CLI::showProgress($currPlatformStep++, $platformType['count']);
$platformLabel = $platform->getAttribute('platform'); $platformSlug = $platform->getAttribute('platform');
$platformSlug = slugify((string) $platformLabel); $platformData = $platforms->findPlatformBySlug($platformType['name'], $platformSlug);
if ($platformModel->getPlatform($platformSlug) instanceof Platform) {
$podcastsPlatformsData[] = [ if ($platformData === null) {
'platform_slug' => $platformSlug, continue;
'podcast_id' => $this->podcast->id,
'link_url' => $platform->getAttribute($platformType['account_url_key']),
'account_id' => $platform->getAttribute($platformType['account_id_key']),
'is_visible' => false,
];
} }
$platformsData[] = [
'podcast_id' => $this->podcast->id,
'type' => $platformType['name'],
'slug' => $platformSlug,
'link_url' => $platform->getAttribute($platformType['account_url_key']),
'account_id' => $platform->getAttribute($platformType['account_id_key']),
'is_visible' => false,
];
} }
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType['name'], $podcastsPlatformsData); $platformModel->savePlatforms($this->podcast->id, $platformType['name'], $platformsData);
CLI::showProgress(false); CLI::showProgress(false);
} }
} }
@ -522,9 +525,7 @@ class PodcastImport extends BaseCommand
->get() ->get()
->getResultArray(); ->getResultArray();
return array_map(static function (array $element) { return array_map(static fn (array $element) => $element['guid'], $result);
return $element['guid'];
}, $result);
} }
/** /**

View File

@ -26,11 +26,10 @@ if (! function_exists('get_import_tasks')) {
} }
if ($podcastHandle !== null) { if ($podcastHandle !== null) {
$podcastImportsQueue = array_filter($podcastImportsQueue, static function ($importTask) use ( $podcastImportsQueue = array_filter(
$podcastHandle $podcastImportsQueue,
): bool { static fn ($importTask): bool => $importTask->handle === $podcastHandle
return $importTask->handle === $podcastHandle; );
});
} }
usort($podcastImportsQueue, static function (PodcastImportTask $a, PodcastImportTask $b): int { usort($podcastImportsQueue, static function (PodcastImportTask $a, PodcastImportTask $b): int {

View File

@ -30,16 +30,16 @@
"dependencies": { "dependencies": {
"@amcharts/amcharts4": "^4.10.38", "@amcharts/amcharts4": "^4.10.38",
"@amcharts/amcharts4-geodata": "^4.1.28", "@amcharts/amcharts4-geodata": "^4.1.28",
"@codemirror/commands": "^6.3.3", "@codemirror/commands": "^6.5.0",
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.10.1", "@codemirror/language": "^6.10.1",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.24.1", "@codemirror/view": "^6.26.3",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.6.3",
"@github/clipboard-copy-element": "^1.3.0", "@github/clipboard-copy-element": "^1.3.0",
"@github/hotkey": "^3.1.0", "@github/hotkey": "^3.1.0",
"@github/markdown-toolbar-element": "^2.2.1", "@github/markdown-toolbar-element": "^2.2.3",
"@github/relative-time-element": "^4.3.1", "@github/relative-time-element": "^4.4.0",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e", "@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@vime/core": "^5.4.1", "@vime/core": "^5.4.1",
"choices.js": "^10.2.0", "choices.js": "^10.2.0",
@ -47,28 +47,28 @@
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"lit": "^3.1.2", "lit": "^3.1.3",
"marked": "^12.0.0", "marked": "^12.0.2",
"wavesurfer.js": "^7.7.3", "wavesurfer.js": "^7.7.11",
"xml-formatter": "^3.6.2" "xml-formatter": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.0.3", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.0.3", "@commitlint/config-conventional": "^19.2.2",
"@csstools/css-tokenizer": "^2.2.3", "@csstools/css-tokenizer": "^2.2.4",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@semantic-release/gitlab": "^13.0.3", "@semantic-release/gitlab": "^13.0.3",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.12",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.12",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.7.1",
"all-contributors-cli": "^6.26.1", "all-contributors-cli": "^6.26.1",
"commitizen": "^4.3.0", "commitizen": "^4.3.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cssnano": "^6.0.3", "cssnano": "^6.1.2",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -76,25 +76,25 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"postcss": "^8.4.35", "postcss": "^8.4.38",
"postcss-import": "^16.0.1", "postcss-import": "^16.1.0",
"postcss-nesting": "^12.0.4", "postcss-nesting": "^12.1.2",
"postcss-preset-env": "^9.4.0", "postcss-preset-env": "^9.5.9",
"postcss-reporter": "^7.1.0", "postcss-reporter": "^7.1.0",
"prettier": "3.2.5", "prettier": "3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"semantic-release": "^23.0.2", "semantic-release": "^23.0.8",
"stylelint": "^16.2.1", "stylelint": "^16.4.0",
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"svgo": "^3.2.0", "svgo": "^3.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.3",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.1.4", "vite": "^5.2.10",
"vite-plugin-pwa": "^0.19.2", "vite-plugin-pwa": "^0.19.8",
"workbox-build": "^7.0.0", "workbox-build": "^7.1.0",
"workbox-core": "^7.0.0", "workbox-core": "^7.1.0",
"workbox-routing": "^7.0.0", "workbox-routing": "^7.1.0",
"workbox-strategies": "^7.0.0" "workbox-strategies": "^7.1.0"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,css,md,json}": "prettier --write", "*.{js,ts,css,md,json}": "prettier --write",

View File

@ -1,7 +1,7 @@
<phpunit <phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="system/Test/bootstrap.php" bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
backupGlobals="false" backupGlobals="false"
beStrictAboutOutputDuringTests="true" beStrictAboutOutputDuringTests="true"
colors="true" colors="true"
@ -57,13 +57,4 @@
<env name="database.tests.DBPrefix" value="tests_"/> <env name="database.tests.DBPrefix" value="tests_"/>
<env name="restapi.enabled" value="true"/> <env name="restapi.enabled" value="true"/>
</php> </php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory suffix=".php">./app/Views</directory>
<file>./app/Config/Routes.php</file>
</exclude>
</source>
</phpunit> </phpunit>

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Rector\CodeQuality\Rector\ClassMethod\ExplicitReturnNullRector;
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector; use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector; use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector;
use Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector; use Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector;
@ -12,31 +13,22 @@ use Rector\EarlyReturn\Rector\If_\ChangeAndIfToEarlyReturnRector;
use Rector\EarlyReturn\Rector\If_\ChangeOrIfContinueToMultiContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeOrIfContinueToMultiContinueRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
use Rector\Set\ValueObject\SetList;
use Rector\ValueObject\PhpVersion; use Rector\ValueObject\PhpVersion;
return static function (RectorConfig $rectorConfig): void { return RectorConfig::configure()
$rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/modules', __DIR__ . '/tests', __DIR__ . '/public']); ->withPaths([__DIR__ . '/app', __DIR__ . '/modules', __DIR__ . '/tests', __DIR__ . '/public'])
->withBootstrapFiles([__DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php'])
// do you need to include constants, class aliases or custom autoloader? files listed will be executed ->withPhpVersion(PhpVersion::PHP_81)
$rectorConfig->bootstrapFiles([__DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php']); ->withPhpSets(php81: true)
->withPreparedSets(
// Define what rule sets will be applied typeDeclarations: true,
$rectorConfig->sets([ codeQuality: true,
SetList::PHP_81, codingStyle: true,
SetList::TYPE_DECLARATION, earlyReturn: true,
SetList::CODE_QUALITY, deadCode: true,
SetList::CODING_STYLE, )
SetList::EARLY_RETURN, ->withImportNames(true, true, true, true)
SetList::DEAD_CODE, ->withSkip([
]);
// auto import fully qualified class names
$rectorConfig->importNames();
$rectorConfig->phpVersion(PhpVersion::PHP_81);
$rectorConfig->skip([
// .mp3 files were somehow processed by rector, so skip all media files // .mp3 files were somehow processed by rector, so skip all media files
__DIR__ . '/public/media/*', __DIR__ . '/public/media/*',
@ -50,6 +42,7 @@ return static function (RectorConfig $rectorConfig): void {
EncapsedStringsToSprintfRector::class, EncapsedStringsToSprintfRector::class,
RemoveExtraParametersRector::class, RemoveExtraParametersRector::class,
UnwrapFutureCompatibleIfPhpVersionRector::class, UnwrapFutureCompatibleIfPhpVersionRector::class,
ExplicitReturnNullRector::class,
// skip rule in specific directory // skip rule in specific directory
StringClassNameToClassConstantRector::class => [ StringClassNameToClassConstantRector::class => [
@ -65,9 +58,5 @@ return static function (RectorConfig $rectorConfig): void {
], ],
ChangeAndIfToEarlyReturnRector::class => [__DIR__ . '/modules/Install/Controllers/InstallController.php'], ChangeAndIfToEarlyReturnRector::class => [__DIR__ . '/modules/Install/Controllers/InstallController.php'],
]); ])
->withPHPStanConfigs([__DIR__ . '/phpstan.neon', 'vendor/codeigniter/phpstan-codeigniter/extension.neon']);
// Path to phpstan with extensions, that PHPStan in Rector uses to determine types
$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');
$rectorConfig->phpstanConfigs(['vendor/codeigniter/phpstan-codeigniter/extension.neon']);
};

2
spark
View File

@ -1,6 +1,8 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
declare(strict_types=1);
/** /**
* This file is part of CodeIgniter 4 framework. * This file is part of CodeIgniter 4 framework.
* *

View File

@ -117,6 +117,7 @@ module.exports = {
cards: "repeat(auto-fill, minmax(14rem, 1fr))", cards: "repeat(auto-fill, minmax(14rem, 1fr))",
latestEpisodes: "repeat(5, 1fr)", latestEpisodes: "repeat(5, 1fr)",
colorButtons: "repeat(auto-fill, minmax(4rem, 1fr))", colorButtons: "repeat(auto-fill, minmax(4rem, 1fr))",
platforms: "repeat(auto-fill, minmax(18rem, 1fr))",
}, },
gridTemplateRows: { gridTemplateRows: {
admin: "40px 1fr", admin: "40px 1fr",

View File

@ -36,8 +36,6 @@ namespace Tests\Support\Libraries;
use Config\App; use Config\App;
/** /**
* Class ConfigReader
*
* An extension of BaseConfig that prevents the constructor from loading external values. Used to read actual local * An extension of BaseConfig that prevents the constructor from loading external values. Used to read actual local
* values from a config file. * values from a config file.
*/ */

View File

@ -14,9 +14,10 @@
<?= $this->section('content') ?> <?= $this->section('content') ?>
<form id="platforms-form" action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="flex flex-col max-w-md gap-y-8"> <form id="platforms-form" action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="grid w-full gap-4 lg:gap-8 grid-cols-platforms">
<?= csrf_field() ?> <?= csrf_field() ?>
<?php foreach ($platforms as $platform): ?> <?php foreach ($platforms as $platform): ?>
<div class="relative flex-col items-start p-4 rounded-lg bg-elevated border-3 <?= $platform->link_url ? 'border-accent-base' : 'border-subtle' ?>"> <div class="relative flex-col items-start p-4 rounded-lg bg-elevated border-3 <?= $platform->link_url ? 'border-accent-base' : 'border-subtle' ?>">
@ -24,7 +25,8 @@
route_to( route_to(
'podcast-platform-remove', 'podcast-platform-remove',
$podcast->id, $podcast->id,
esc($platform->slug), $platform->type,
$platform->slug,
), ),
icon('delete-bin', 'mx-auto'), icon('delete-bin', 'mx-auto'),
[ [
@ -36,7 +38,7 @@
], ],
) )
: '' ?> : '' ?>
<div class="flex items-center gap-x-4"> <div class="flex items-center gap-x-2">
<?= icon( <?= icon(
esc($platform->slug), esc($platform->slug),
'text-skin-muted text-4xl', 'text-skin-muted text-4xl',
@ -45,29 +47,15 @@
<h2 class="text-xl font-semibold"><?= $platform->label ?></h2> <h2 class="text-xl font-semibold"><?= $platform->label ?></h2>
</div> </div>
<div class="flex flex-col flex-1 mt-4"> <div class="flex flex-col flex-1 mt-4">
<div class="inline-flex ml-12 gap-x-2"> <div class="inline-flex ml-8 -mt-6 gap-x-1">
<?= anchor($platform->home_url, icon('external-link', 'mx-auto') . lang('Platforms.website'), [ <Button uri="<?= $platform->home_url ?>" variant="link" size="small" target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.home_url', [
'class' => 'gap-x-1 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent px-2 py-1 text-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover', 'platformName' => $platform->label,
'target' => '_blank', ]) ?>" data-tooltip="bottom"><?= lang('Platforms.website') ?></Button>
'rel' => 'noopener noreferrer', <?php if ($platform->submit_url !== null): ?>
'data-tooltip' => 'bottom', <Button uri="<?= $platform->submit_url ?>" variant="link" size="small" target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.submit_url', [
'title' => lang('Platforms.home_url', [
'platformName' => $platform->label, 'platformName' => $platform->label,
]), ]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></Button>
]) ?> <?php endif; ?>
<?= $platform->submit_url ? anchor(
$platform->submit_url,
icon('add') . lang('Platforms.register'),
[
'class' => 'gap-x-1 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent px-2 py-1 text-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'target' => '_blank',
'rel' => 'noopener noreferrer',
'data-tooltip' => 'bottom',
'title' => lang('Platforms.submit_url', [
'platformName' => $platform->label,
]),
]
) : '' ?>
</div> </div>
<fieldset> <fieldset>
<Forms.Field <Forms.Field