feat(public-ui): adapt public podcast and episode pages to wireframes
- adapt wireframes with responsive design - refactor models methods to cache requests for faster queries - update public controllers to cache pages while retaining analytics hits - add platform links to podcast page - add previous / next episodes in episode page - update npm packages to latest versions closes #30, #13
This commit is contained in:
parent
2517808cd4
commit
40a0535fc1
|
@ -26,7 +26,7 @@ class Contributor extends BaseController
|
|||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
$this->podcast = (new PodcastModel())->find($params[0]);
|
||||
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
|
||||
|
||||
if (count($params) > 1) {
|
||||
if (
|
||||
|
|
|
@ -25,7 +25,7 @@ class Episode extends BaseController
|
|||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
$this->podcast = (new PodcastModel())->find($params[0]);
|
||||
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
|
||||
|
||||
if (count($params) > 1) {
|
||||
if (
|
||||
|
|
|
@ -25,7 +25,11 @@ class Podcast extends BaseController
|
|||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (count($params) > 0) {
|
||||
if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastById(
|
||||
$params[0]
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
@ -58,26 +62,8 @@ class Podcast extends BaseController
|
|||
{
|
||||
helper(['form', 'misc']);
|
||||
|
||||
$categories = (new CategoryModel())->findAll();
|
||||
$languages = (new LanguageModel())->findAll();
|
||||
$languageOptions = array_reduce(
|
||||
$languages,
|
||||
function ($result, $language) {
|
||||
$result[$language->code] = $language->native_name;
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$categoryOptions = array_reduce(
|
||||
$categories,
|
||||
function ($result, $category) {
|
||||
$result[$category->id] = lang(
|
||||
'Podcast.category_options.' . $category->code
|
||||
);
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$languageOptions = (new LanguageModel())->getLanguageOptions();
|
||||
$categoryOptions = (new CategoryModel())->getCategoryOptions();
|
||||
|
||||
$data = [
|
||||
'languageOptions' => $languageOptions,
|
||||
|
@ -157,26 +143,8 @@ class Podcast extends BaseController
|
|||
{
|
||||
helper(['form', 'misc']);
|
||||
|
||||
$categories = (new CategoryModel())->findAll();
|
||||
$languages = (new LanguageModel())->findAll();
|
||||
$languageOptions = array_reduce(
|
||||
$languages,
|
||||
function ($result, $language) {
|
||||
$result[$language->code] = $language->native_name;
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$categoryOptions = array_reduce(
|
||||
$categories,
|
||||
function ($result, $category) {
|
||||
$result[$category->id] = lang(
|
||||
'Podcast.category_options.' . $category->code
|
||||
);
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$languageOptions = (new LanguageModel())->getLanguageOptions();
|
||||
$categoryOptions = (new CategoryModel())->getCategoryOptions();
|
||||
|
||||
$data = [
|
||||
'languageOptions' => $languageOptions,
|
||||
|
@ -373,26 +341,8 @@ class Podcast extends BaseController
|
|||
{
|
||||
helper('form');
|
||||
|
||||
$categories = (new CategoryModel())->findAll();
|
||||
$languages = (new LanguageModel())->findAll();
|
||||
$languageOptions = array_reduce(
|
||||
$languages,
|
||||
function ($result, $language) {
|
||||
$result[$language->code] = $language->native_name;
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$categoryOptions = array_reduce(
|
||||
$categories,
|
||||
function ($result, $category) {
|
||||
$result[$category->id] = lang(
|
||||
'Podcast.category_options.' . $category->code
|
||||
);
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$languageOptions = (new LanguageModel())->getLanguageOptions();
|
||||
$categoryOptions = (new CategoryModel())->getCategoryOptions();
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
|
|
|
@ -21,7 +21,9 @@ class PodcastSettings extends BaseController
|
|||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
unset($params[0]);
|
||||
|
@ -40,7 +42,9 @@ class PodcastSettings extends BaseController
|
|||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'platforms' => (new PlatformModel())->getPlatformsWithLinks(),
|
||||
'platforms' => (new PlatformModel())->getPlatformsWithLinks(
|
||||
$this->podcast->id
|
||||
),
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([0 => $this->podcast->title]);
|
||||
|
|
|
@ -26,7 +26,7 @@ class BaseController extends Controller
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['analytics'];
|
||||
protected $helpers = ['analytics', 'svg'];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
|
|
@ -25,18 +25,14 @@ class Episode extends BaseController
|
|||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
$this->podcast = (new PodcastModel())
|
||||
->where('name', $params[0])
|
||||
->first();
|
||||
$this->podcast = (new PodcastModel())->getPodcastByName($params[0]);
|
||||
|
||||
if (
|
||||
count($params) > 1 &&
|
||||
!($this->episode = (new EpisodeModel())
|
||||
->where([
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'slug' => $params[1],
|
||||
])
|
||||
->first())
|
||||
!($this->episode = (new EpisodeModel())->getEpisodeBySlug(
|
||||
$this->podcast->id,
|
||||
$params[1]
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
@ -46,15 +42,31 @@ class Episode extends BaseController
|
|||
|
||||
public function index()
|
||||
{
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
$this->cachePage(DECADE);
|
||||
self::triggerWebpageHit($this->episode->podcast_id);
|
||||
|
||||
self::triggerWebpageHit($this->podcast->id);
|
||||
if (
|
||||
!($cachedView = cache(
|
||||
"page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}"
|
||||
))
|
||||
) {
|
||||
$previousNextEpisodes = (new EpisodeModel())->getPreviousNextEpisodes(
|
||||
$this->episode,
|
||||
$this->podcast->type
|
||||
);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
return view('episode', $data);
|
||||
$data = [
|
||||
'previousEpisode' => $previousNextEpisodes['previous'],
|
||||
'nextEpisode' => $previousNextEpisodes['next'],
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('episode', $data, [
|
||||
'cache' => DECADE,
|
||||
'cache_name' => "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}",
|
||||
]);
|
||||
}
|
||||
|
||||
return $cachedView;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
|
||||
class Podcast extends BaseController
|
||||
|
@ -21,9 +22,9 @@ class Podcast extends BaseController
|
|||
{
|
||||
if (count($params) > 0) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())
|
||||
->where('name', $params[0])
|
||||
->first())
|
||||
!($this->podcast = (new PodcastModel())->getPodcastByName(
|
||||
$params[0]
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
@ -34,15 +35,100 @@ class Podcast extends BaseController
|
|||
|
||||
public function index()
|
||||
{
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
$this->cachePage(DECADE);
|
||||
|
||||
self::triggerWebpageHit($this->podcast->id);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episodes' => $this->podcast->episodes,
|
||||
];
|
||||
return view('podcast', $data);
|
||||
$yearQuery = $this->request->getGet('year');
|
||||
$seasonQuery = $this->request->getGet('season');
|
||||
|
||||
if (!$yearQuery and !$seasonQuery) {
|
||||
$defaultQuery = (new EpisodeModel())->getDefaultQuery(
|
||||
$this->podcast->id
|
||||
);
|
||||
if ($defaultQuery['type'] == 'season') {
|
||||
$seasonQuery = $defaultQuery['data']['season_number'];
|
||||
} elseif ($defaultQuery['type'] == 'year') {
|
||||
$yearQuery = $defaultQuery['data']['year'];
|
||||
}
|
||||
}
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
array_filter([
|
||||
'page',
|
||||
"podcast{$this->podcast->id}",
|
||||
$yearQuery,
|
||||
$seasonQuery ? 'season' . $seasonQuery : null,
|
||||
])
|
||||
);
|
||||
|
||||
if (!($found = cache($cacheName))) {
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
// $this->cachePage(DECADE);
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
||||
// Build navigation array
|
||||
$years = $episodeModel->getYears($this->podcast->id);
|
||||
$seasons = $episodeModel->getSeasons($this->podcast->id);
|
||||
|
||||
$episodesNavigation = [];
|
||||
$activeQuery = null;
|
||||
foreach ($years as $year) {
|
||||
$isActive = $yearQuery == $year['year'];
|
||||
if ($isActive) {
|
||||
$activeQuery = ['type' => 'year', 'value' => $year['year']];
|
||||
}
|
||||
|
||||
array_push($episodesNavigation, [
|
||||
'label' => $year['year'],
|
||||
'number_of_episodes' => $year['number_of_episodes'],
|
||||
'route' =>
|
||||
route_to('podcast', $this->podcast->name) .
|
||||
'?year=' .
|
||||
$year['year'],
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($seasons as $season) {
|
||||
$isActive = $seasonQuery == $season['season_number'];
|
||||
if ($isActive) {
|
||||
$activeQuery = [
|
||||
'type' => 'season',
|
||||
'value' => $season['season_number'],
|
||||
];
|
||||
}
|
||||
|
||||
array_push($episodesNavigation, [
|
||||
'label' => lang('Podcast.season', [
|
||||
'seasonNumber' => $season['season_number'],
|
||||
]),
|
||||
'number_of_episodes' => $season['number_of_episodes'],
|
||||
'route' =>
|
||||
route_to('podcast', $this->podcast->name) .
|
||||
'?season=' .
|
||||
$season['season_number'],
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episodesNav' => $episodesNavigation,
|
||||
'activeQuery' => $activeQuery,
|
||||
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
|
||||
$this->podcast->id,
|
||||
$this->podcast->type,
|
||||
$yearQuery,
|
||||
$seasonQuery
|
||||
),
|
||||
];
|
||||
|
||||
return view('podcast', $data, [
|
||||
'cache' => DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,22 @@ class AddEpisodes extends Migration
|
|||
'type' => 'VARCHAR',
|
||||
'constraint' => 1024,
|
||||
],
|
||||
'enclosure_duration' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 10,
|
||||
'unsigned' => true,
|
||||
'comment' => 'Playtime in seconds',
|
||||
],
|
||||
'enclosure_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'enclosure_filesize' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 10,
|
||||
'unsigned' => true,
|
||||
'comment' => 'File size in bytes',
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
|
|
|
@ -36,6 +36,7 @@ class AddAnalyticsEpisodesByCountry extends Migration
|
|||
'country_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 3,
|
||||
'comment' => 'ISO 3166-1 code.',
|
||||
],
|
||||
'date' => [
|
||||
'type' => 'date',
|
||||
|
|
|
@ -54,11 +54,6 @@ class Episode extends Entity
|
|||
*/
|
||||
protected $enclosure_url;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $enclosure_metadata;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -76,6 +71,9 @@ class Episode extends Entity
|
|||
'slug' => 'string',
|
||||
'title' => 'string',
|
||||
'enclosure_uri' => 'string',
|
||||
'enclosure_duration' => 'integer',
|
||||
'enclosure_mimetype' => 'string',
|
||||
'enclosure_filesize' => 'integer',
|
||||
'description' => 'string',
|
||||
'image_uri' => '?string',
|
||||
'explicit' => 'boolean',
|
||||
|
@ -106,19 +104,6 @@ class Episode extends Entity
|
|||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
);
|
||||
} elseif (
|
||||
$APICdata = $this->getEnclosureMetadata()['attached_picture']
|
||||
) {
|
||||
// if the user didn't input an image,
|
||||
// check if the uploaded audio file has an attached cover and store it
|
||||
$cover_image = new \CodeIgniter\Files\File('episode_cover');
|
||||
file_put_contents($cover_image, $APICdata);
|
||||
|
||||
$this->attributes['image_uri'] = save_podcast_media(
|
||||
$cover_image,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
@ -155,13 +140,22 @@ class Episode extends Entity
|
|||
(!($enclosure instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
|
||||
$enclosure->isValid())
|
||||
) {
|
||||
helper('media');
|
||||
helper(['media', 'id3']);
|
||||
|
||||
$enclosure_metadata = get_file_tags($enclosure);
|
||||
|
||||
$this->attributes['enclosure_uri'] = save_podcast_media(
|
||||
$enclosure,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
);
|
||||
$this->attributes['enclosure_duration'] = round(
|
||||
$enclosure_metadata['playtime_seconds']
|
||||
);
|
||||
$this->attributes['enclosure_mimetype'] =
|
||||
$enclosure_metadata['mime_type'];
|
||||
$this->attributes['enclosure_filesize'] =
|
||||
$enclosure_metadata['filesize'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -191,13 +185,6 @@ class Episode extends Entity
|
|||
);
|
||||
}
|
||||
|
||||
public function getEnclosureMetadata()
|
||||
{
|
||||
helper('id3');
|
||||
|
||||
return get_file_tags($this->getEnclosure());
|
||||
}
|
||||
|
||||
public function getLink()
|
||||
{
|
||||
return base_url(
|
||||
|
@ -218,7 +205,9 @@ class Episode extends Entity
|
|||
|
||||
public function getPodcast()
|
||||
{
|
||||
return (new PodcastModel())->find($this->attributes['podcast_id']);
|
||||
return (new PodcastModel())->getPodcastById(
|
||||
$this->attributes['podcast_id']
|
||||
);
|
||||
}
|
||||
|
||||
public function getDescriptionHtml()
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PlatformModel;
|
||||
use CodeIgniter\Entity;
|
||||
use App\Models\UserModel;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
@ -40,6 +42,11 @@ class Podcast extends Entity
|
|||
*/
|
||||
protected $episodes;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Category
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\User[]
|
||||
*/
|
||||
|
@ -50,6 +57,11 @@ class Podcast extends Entity
|
|||
*/
|
||||
protected $description_html;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Platform
|
||||
*/
|
||||
protected $platforms;
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'title' => 'string',
|
||||
|
@ -134,13 +146,34 @@ class Podcast extends Entity
|
|||
|
||||
if (empty($this->episodes)) {
|
||||
$this->episodes = (new EpisodeModel())->getPodcastEpisodes(
|
||||
$this->id
|
||||
$this->id,
|
||||
$this->type
|
||||
);
|
||||
}
|
||||
|
||||
return $this->episodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the podcast category entity
|
||||
*
|
||||
* @return \App\Entities\Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting category.'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->category)) {
|
||||
$this->category = (new CategoryModel())->find($this->category_id);
|
||||
}
|
||||
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all podcast contributors
|
||||
*
|
||||
|
@ -186,4 +219,26 @@ class Podcast extends Entity
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the podcast's platform links
|
||||
*
|
||||
* @return \App\Entities\Platform[]
|
||||
*/
|
||||
public function getPlatforms()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting platform links.'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->platforms)) {
|
||||
$this->platforms = (new PlatformModel())->getPodcastPlatformLinks(
|
||||
$this->id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->platforms;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,9 @@ class User extends \Myth\Auth\Entities\User
|
|||
}
|
||||
|
||||
if (empty($this->podcast)) {
|
||||
$this->podcast = (new PodcastModel())->find($this->podcast_id);
|
||||
$this->podcast = (new PodcastModel())->getPodcastById(
|
||||
$this->podcast_id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->podcast;
|
||||
|
|
|
@ -25,9 +25,6 @@ function get_file_tags($file)
|
|||
'filesize' => $FileInfo['filesize'],
|
||||
'mime_type' => $FileInfo['mime_type'],
|
||||
'playtime_seconds' => $FileInfo['playtime_seconds'],
|
||||
'attached_picture' => array_key_exists('comments', $FileInfo)
|
||||
? $FileInfo['comments']['picture'][0]['data']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -11,17 +11,20 @@ use App\Models\PageModel;
|
|||
/**
|
||||
* Returns instance pages as links inside nav tag
|
||||
*
|
||||
* @param string $class
|
||||
* @return string html pages navigation
|
||||
*/
|
||||
function render_page_links()
|
||||
function render_page_links($class = null)
|
||||
{
|
||||
$pages = (new PageModel())->findAll();
|
||||
$links = '';
|
||||
$links = anchor(route_to('home'), lang('Common.home'), [
|
||||
'class' => 'px-2 underline hover:no-underline',
|
||||
]);
|
||||
foreach ($pages as $page) {
|
||||
$links .= anchor($page->link, $page->title, [
|
||||
'class' => 'px-2 underline hover:no-underline',
|
||||
]);
|
||||
}
|
||||
|
||||
return '<nav class="inline-flex">' . $links . '</nav>';
|
||||
return '<nav class="' . $class . '">' . $links . '</nav>';
|
||||
}
|
||||
|
|
|
@ -115,10 +115,9 @@ function get_rss_feed($podcast)
|
|||
$item->addChild('title', $episode->title);
|
||||
$enclosure = $item->addChild('enclosure');
|
||||
|
||||
$enclosure_metadata = $episode->enclosure_metadata;
|
||||
$enclosure->addAttribute('url', $episode->enclosure_url);
|
||||
$enclosure->addAttribute('length', $enclosure_metadata['filesize']);
|
||||
$enclosure->addAttribute('type', $enclosure_metadata['mime_type']);
|
||||
$enclosure->addAttribute('length', $episode->enclosure_filesize);
|
||||
$enclosure->addAttribute('type', $episode->enclosure_mimetype);
|
||||
|
||||
$item->addChild('guid', $episode->guid);
|
||||
$item->addChild(
|
||||
|
@ -128,7 +127,7 @@ function get_rss_feed($podcast)
|
|||
$item->addChildWithCDATA('description', $episode->description_html);
|
||||
$item->addChild(
|
||||
'duration',
|
||||
$enclosure_metadata['playtime_seconds'],
|
||||
$episode->enclosure_duration,
|
||||
$itunes_namespace
|
||||
);
|
||||
$item->addChild('link', $episode->link);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'home' => 'Home',
|
||||
'explicit' => 'Explicit',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'duration' => '{0,duration}',
|
||||
'powered_by' => 'Powered by {castopod}.',
|
||||
];
|
|
@ -7,6 +7,12 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'previous_episode' => 'Previous episode',
|
||||
'previous_season' => 'Previous season',
|
||||
'next_episode' => 'Next episode',
|
||||
'next_season' => 'Next season',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'number' => 'Episode {episodeNumber}',
|
||||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'edit' => 'Edit',
|
||||
|
|
|
@ -219,6 +219,11 @@ return [
|
|||
'film_reviews' => 'Film Reviews',
|
||||
'tv_reviews' => 'TV Reviews',
|
||||
],
|
||||
'list_of_episodes' => 'List of episodes',
|
||||
'no_episode' => 'No episode found',
|
||||
'by' => 'By {author}',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'list_of_episodes_year' => '{year} episodes',
|
||||
'list_of_episodes_season' => 'Season {seasonNumber} episodes',
|
||||
'no_episode' => 'No episode found!',
|
||||
'no_episode_hint' =>
|
||||
'Navigate the podcast episodes with the navigation bar above.',
|
||||
];
|
||||
|
|
|
@ -31,4 +31,26 @@ class CategoryModel extends Model
|
|||
{
|
||||
return $this->find($parentId);
|
||||
}
|
||||
|
||||
public function getCategoryOptions()
|
||||
{
|
||||
if (!($options = cache('category_options'))) {
|
||||
$categories = $this->findAll();
|
||||
|
||||
$options = array_reduce(
|
||||
$categories,
|
||||
function ($result, $category) {
|
||||
$result[$category->id] = lang(
|
||||
'Podcast.category_options.' . $category->code
|
||||
);
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
cache()->save('category_options', $options, DECADE);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ class EpisodeModel extends Model
|
|||
'title',
|
||||
'slug',
|
||||
'enclosure_uri',
|
||||
'enclosure_duration',
|
||||
'enclosure_mimetype',
|
||||
'enclosure_filesize',
|
||||
'description',
|
||||
'image_uri',
|
||||
'explicit',
|
||||
|
@ -75,7 +78,9 @@ class EpisodeModel extends Model
|
|||
|
||||
protected function clearCache(array $data)
|
||||
{
|
||||
$episode = (new EpisodeModel())->find(
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
||||
$episode = $episodeModel->find(
|
||||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
|
@ -85,26 +90,213 @@ class EpisodeModel extends Model
|
|||
cache()->delete(md5($episode->link));
|
||||
|
||||
// delete model requests cache
|
||||
cache()->delete("{$episode->podcast_id}_episodes");
|
||||
cache()->delete("podcast{$episode->podcast_id}_episodes");
|
||||
|
||||
// delete episode lists cache per year / season
|
||||
$years = $episodeModel->getYears($episode->podcast_id);
|
||||
$seasons = $episodeModel->getSeasons($episode->podcast_id);
|
||||
|
||||
foreach ($years as $year) {
|
||||
cache()->delete(
|
||||
"podcast{$episode->podcast_id}_{$year['year']}_episodes"
|
||||
);
|
||||
cache()->delete(
|
||||
"page_podcast{$episode->podcast_id}_{$year['year']}"
|
||||
);
|
||||
}
|
||||
foreach ($seasons as $season) {
|
||||
cache()->delete(
|
||||
"podcast{$episode->podcast_id}_season{$season['season_number']}_episodes"
|
||||
);
|
||||
cache()->delete(
|
||||
"page_podcast{$episode->podcast_id}_season{$season['season_number']}"
|
||||
);
|
||||
}
|
||||
|
||||
cache()->delete("podcast{$episode->podcast_id}_defaultQuery");
|
||||
cache()->delete("podcast{$episode->podcast_id}_years");
|
||||
cache()->delete("podcast{$episode->podcast_id}_seasons");
|
||||
|
||||
cache()->delete(
|
||||
"podcast{$episode->podcast_id}_episode@{$episode->slug}"
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getEpisodeBySlug($podcastId, $episodeSlug)
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}_episode@{$episodeSlug}"))) {
|
||||
$found = $this->where([
|
||||
'podcast_id' => $podcastId,
|
||||
'slug' => $episodeSlug,
|
||||
])->first();
|
||||
|
||||
cache()->save(
|
||||
"podcast{$podcastId}_episode@{$episodeSlug}",
|
||||
$found,
|
||||
DECADE
|
||||
);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all episodes for a podcast
|
||||
* Gets all episodes for a podcast ordered according to podcast type
|
||||
* Filtered depending on year or season
|
||||
*
|
||||
* @param int $podcastId
|
||||
*
|
||||
* @return \App\Entities\Episode[]
|
||||
*/
|
||||
public function getPodcastEpisodes(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("{$podcastId}_episodes"))) {
|
||||
$found = $this->where('podcast_id', $podcastId)->findAll();
|
||||
public function getPodcastEpisodes(
|
||||
int $podcastId,
|
||||
string $podcastType,
|
||||
string $year = null,
|
||||
string $season = null
|
||||
): array {
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
array_filter([
|
||||
"podcast{$podcastId}",
|
||||
$year,
|
||||
$season ? 'season' . $season : null,
|
||||
'episodes',
|
||||
])
|
||||
);
|
||||
|
||||
cache()->save("{$podcastId}_episodes", $found, 300);
|
||||
if (!($found = cache($cacheName))) {
|
||||
$where = ['podcast_id' => $podcastId];
|
||||
if ($year) {
|
||||
$where['YEAR(published_at)'] = $year;
|
||||
$where['season_number'] = null;
|
||||
}
|
||||
if ($season) {
|
||||
$where['season_number'] = $season;
|
||||
}
|
||||
|
||||
if ($podcastType == 'serial') {
|
||||
// podcast is serial
|
||||
$found = $this->where($where)
|
||||
->orderBy('season_number DESC, number ASC')
|
||||
->findAll();
|
||||
} else {
|
||||
$found = $this->where($where)
|
||||
->orderBy('published_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
cache()->save($cacheName, $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getYears(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}_years"))) {
|
||||
$found = $this->select(
|
||||
'YEAR(published_at) as year, count(*) as number_of_episodes'
|
||||
)
|
||||
->where(['podcast_id' => $podcastId, 'season_number' => null])
|
||||
->groupBy('year')
|
||||
->orderBy('year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
cache()->save("podcast{$podcastId}_years", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getSeasons(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}_seasons"))) {
|
||||
$found = $this->select(
|
||||
'season_number, count(*) as number_of_episodes'
|
||||
)
|
||||
->where([
|
||||
'podcast_id' => $podcastId,
|
||||
'season_number is not' => null,
|
||||
])
|
||||
->groupBy('season_number')
|
||||
->orderBy('season_number', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
cache()->save("podcast{$podcastId}_seasons", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default query for displaying the episode list on the podcast page
|
||||
*/
|
||||
public function getDefaultQuery(int $podcastId)
|
||||
{
|
||||
if (!($defaultQuery = cache("podcast{$podcastId}_defaultQuery"))) {
|
||||
$seasons = $this->getSeasons($podcastId);
|
||||
|
||||
if (!empty($seasons)) {
|
||||
// get latest season
|
||||
$defaultQuery = ['type' => 'season', 'data' => end($seasons)];
|
||||
} else {
|
||||
$years = $this->getYears($podcastId);
|
||||
if (!empty($years)) {
|
||||
// get most recent year
|
||||
$defaultQuery = ['type' => 'year', 'data' => $years[0]];
|
||||
} else {
|
||||
$defaultQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
cache()->save(
|
||||
"podcast{$podcastId}_defaultQuery",
|
||||
$defaultQuery,
|
||||
DECADE
|
||||
);
|
||||
}
|
||||
return $defaultQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous episode based on episode ordering
|
||||
*/
|
||||
public function getPreviousNextEpisodes($episode, $podcastType)
|
||||
{
|
||||
$sortNumberField =
|
||||
$podcastType == 'serial'
|
||||
? 'if(isnull(season_number),0,season_number)*1000+number'
|
||||
: 'UNIX_TIMESTAMP(published_at)';
|
||||
$sortNumberValue =
|
||||
$podcastType == 'serial'
|
||||
? (empty($episode->season_number)
|
||||
? 0
|
||||
: $episode->season_number) *
|
||||
1000 +
|
||||
$episode->number
|
||||
: strtotime($episode->published_at);
|
||||
|
||||
$previousData = $this->orderBy('(' . $sortNumberField . ') DESC')
|
||||
->where([
|
||||
'podcast_id' => $episode->podcast_id,
|
||||
$sortNumberField . ' <' => $sortNumberValue,
|
||||
])
|
||||
->first();
|
||||
|
||||
$nextData = $this->orderBy('(' . $sortNumberField . ') ASC')
|
||||
->where([
|
||||
'podcast_id' => $episode->podcast_id,
|
||||
$sortNumberField . ' >' => $sortNumberValue,
|
||||
])
|
||||
->first();
|
||||
|
||||
return [
|
||||
'previous' => $previousData,
|
||||
'next' => $nextData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,24 @@ class LanguageModel extends Model
|
|||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function getLanguageOptions()
|
||||
{
|
||||
if (!($options = cache('language_options'))) {
|
||||
$languages = $this->findAll();
|
||||
|
||||
$options = array_reduce(
|
||||
$languages,
|
||||
function ($result, $language) {
|
||||
$result[$language->code] = $language->native_name;
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
cache()->save('language_options', $options, DECADE);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,21 +36,49 @@ class PlatformModel extends Model
|
|||
|
||||
protected $useTimestamps = true;
|
||||
|
||||
public function getPlatformsWithLinks()
|
||||
public function getPlatformsWithLinks($podcastId)
|
||||
{
|
||||
return $this->select(
|
||||
'platforms.*, platform_links.link_url, platform_links.visible'
|
||||
)
|
||||
->join(
|
||||
'platform_links',
|
||||
'platform_links.platform_id = platforms.id',
|
||||
'left'
|
||||
if (!($found = cache("podcast{$podcastId}_platforms"))) {
|
||||
$found = $this->select(
|
||||
'platforms.*, platform_links.link_url, platform_links.visible'
|
||||
)
|
||||
->findAll();
|
||||
->join(
|
||||
'platform_links',
|
||||
"platform_links.platform_id = platforms.id AND platform_links.podcast_id = $podcastId",
|
||||
'left'
|
||||
)
|
||||
->findAll();
|
||||
|
||||
cache()->save("podcast{$podcastId}_platforms", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getPodcastPlatformLinks($podcastId)
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}_platformLinks"))) {
|
||||
$found = $this->select(
|
||||
'platforms.*, platform_links.link_url, platform_links.visible'
|
||||
)
|
||||
->join(
|
||||
'platform_links',
|
||||
'platform_links.platform_id = platforms.id'
|
||||
)
|
||||
->where('platform_links.podcast_id', $podcastId)
|
||||
->findAll();
|
||||
|
||||
cache()->save("podcast{$podcastId}_platformLinks", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function savePlatformLinks($podcastId, $platformLinksData)
|
||||
{
|
||||
cache()->delete("podcast{$podcastId}_platforms");
|
||||
cache()->delete("podcast{$podcastId}_platformLinks");
|
||||
|
||||
// Remove already previously set platforms to overwrite them
|
||||
$this->db
|
||||
->table('platform_links')
|
||||
|
@ -81,6 +109,9 @@ class PlatformModel extends Model
|
|||
|
||||
public function removePlatformLink($podcastId, $platformId)
|
||||
{
|
||||
cache()->delete("podcast{$podcastId}_platforms");
|
||||
cache()->delete("podcast{$podcastId}_platformLinks");
|
||||
|
||||
return $this->db->table('platform_links')->delete([
|
||||
'podcast_id' => $podcastId,
|
||||
'platform_id' => $platformId,
|
||||
|
|
|
@ -62,63 +62,99 @@ class PodcastModel extends Model
|
|||
protected $beforeUpdate = ['clearCache'];
|
||||
protected $beforeDelete = ['clearCache'];
|
||||
|
||||
public function getPodcastByName($podcastName)
|
||||
{
|
||||
if (!($found = cache("podcast@{$podcastName}"))) {
|
||||
$found = $this->where('name', $podcastName)->first();
|
||||
|
||||
cache()->save("podcast@{$podcastName}", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getPodcastById($podcastId)
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}"))) {
|
||||
$found = $this->find($podcastId);
|
||||
|
||||
cache()->save("podcast{$podcastId}", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the podcasts a given user is contributing to
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $userId
|
||||
*
|
||||
* @return \App\Entities\Podcast[] podcasts
|
||||
*/
|
||||
public function getUserPodcasts($user_id)
|
||||
public function getUserPodcasts($userId)
|
||||
{
|
||||
return $this->select('podcasts.*')
|
||||
->join('users_podcasts', 'users_podcasts.podcast_id = podcasts.id')
|
||||
->where('users_podcasts.user_id', $user_id)
|
||||
->findAll();
|
||||
if (!($found = cache("user{$userId}_podcasts"))) {
|
||||
$found = $this->select('podcasts.*')
|
||||
->join(
|
||||
'users_podcasts',
|
||||
'users_podcasts.podcast_id = podcasts.id'
|
||||
)
|
||||
->where('users_podcasts.user_id', $userId)
|
||||
->findAll();
|
||||
|
||||
cache()->save("user{$userId}_podcasts", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function addPodcastContributor($user_id, $podcast_id, $group_id)
|
||||
public function addPodcastContributor($userId, $podcastId, $groupId)
|
||||
{
|
||||
cache()->delete("podcast{$podcastId}_contributors");
|
||||
|
||||
$data = [
|
||||
'user_id' => (int) $user_id,
|
||||
'podcast_id' => (int) $podcast_id,
|
||||
'group_id' => (int) $group_id,
|
||||
'user_id' => (int) $userId,
|
||||
'podcast_id' => (int) $podcastId,
|
||||
'group_id' => (int) $groupId,
|
||||
];
|
||||
|
||||
return $this->db->table('users_podcasts')->insert($data);
|
||||
}
|
||||
|
||||
public function updatePodcastContributor($user_id, $podcast_id, $group_id)
|
||||
public function updatePodcastContributor($userId, $podcastId, $groupId)
|
||||
{
|
||||
cache()->delete("podcast{$podcastId}_contributors");
|
||||
|
||||
return $this->db
|
||||
->table('users_podcasts')
|
||||
->where([
|
||||
'user_id' => (int) $user_id,
|
||||
'podcast_id' => (int) $podcast_id,
|
||||
'user_id' => (int) $userId,
|
||||
'podcast_id' => (int) $podcastId,
|
||||
])
|
||||
->update(['group_id' => $group_id]);
|
||||
->update(['group_id' => $groupId]);
|
||||
}
|
||||
|
||||
public function removePodcastContributor($user_id, $podcast_id)
|
||||
public function removePodcastContributor($userId, $podcastId)
|
||||
{
|
||||
cache()->delete("podcast{$podcastId}_contributors");
|
||||
|
||||
return $this->db
|
||||
->table('users_podcasts')
|
||||
->where([
|
||||
'user_id' => $user_id,
|
||||
'podcast_id' => $podcast_id,
|
||||
'user_id' => $userId,
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function getContributorGroupId($user_id, $podcast_id)
|
||||
public function getContributorGroupId($userId, $podcastId)
|
||||
{
|
||||
// TODO: return only the group id
|
||||
$user_podcast = $this->db
|
||||
->table('users_podcasts')
|
||||
->select('group_id')
|
||||
->where([
|
||||
'user_id' => $user_id,
|
||||
'podcast_id' => $podcast_id,
|
||||
'user_id' => $userId,
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
@ -130,7 +166,7 @@ class PodcastModel extends Model
|
|||
|
||||
protected function clearCache(array $data)
|
||||
{
|
||||
$podcast = (new PodcastModel())->find(
|
||||
$podcast = (new PodcastModel())->getPodcastById(
|
||||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
|
@ -143,6 +179,10 @@ class PodcastModel extends Model
|
|||
cache()->delete(md5($episode->link));
|
||||
}
|
||||
|
||||
// delete model requests cache
|
||||
cache()->delete("podcast{$podcast->id}");
|
||||
cache()->delete("podcast@{$podcast->name}");
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,33 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class UserModel extends \Myth\Auth\Models\UserModel
|
||||
{
|
||||
protected $returnType = \App\Entities\User::class;
|
||||
|
||||
public function getPodcastContributors($podcast_id)
|
||||
public function getPodcastContributors($podcastId)
|
||||
{
|
||||
return $this->select('users.*, auth_groups.name as podcast_role')
|
||||
->join('users_podcasts', 'users_podcasts.user_id = users.id')
|
||||
->join('auth_groups', 'auth_groups.id = users_podcasts.group_id')
|
||||
->where('users_podcasts.podcast_id', $podcast_id)
|
||||
->findAll();
|
||||
if (!($found = cache("podcast{$podcastId}_contributors"))) {
|
||||
$found = $this->select('users.*, auth_groups.name as podcast_role')
|
||||
->join('users_podcasts', 'users_podcasts.user_id = users.id')
|
||||
->join(
|
||||
'auth_groups',
|
||||
'auth_groups.id = users_podcasts.group_id'
|
||||
)
|
||||
->where('users_podcasts.podcast_id', $podcastId)
|
||||
->findAll();
|
||||
|
||||
cache()->save("podcast{$podcastId}_contributors", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getPodcastContributor($user_id, $podcast_id)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 214 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M13.172 12l-4.95-4.95 1.414-1.414L16 12l-6.364 6.364-1.414-1.414z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 217 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M3 17a4 4 0 0 1 4 4H3v-4zm0-7c6.075 0 11 4.925 11 11h-2a9 9 0 0 0-9-9v-2zm0-7c9.941 0 18 8.059 18 18h-2c0-8.837-7.163-16-16-16V3z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 281 B |
|
@ -1,11 +1,103 @@
|
|||
<?= $this->extend('_layout') ?>
|
||||
<?= helper('page') ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= $episode->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title><?= $episode->title ?></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="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
</head>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<body class="flex flex-col min-h-screen mx-auto">
|
||||
<header class="border-b bg-gradient-to-tr from-gray-900 to-gray-800">
|
||||
<div class="container flex items-start px-2 py-2 mx-auto">
|
||||
<img class="w-12 h-12 mr-2 rounded cover" src="<?= $episode->podcast
|
||||
->image_url ?>" alt="<?= $episode->podcast->title ?>" />
|
||||
<a href="<?= route_to(
|
||||
'podcast',
|
||||
$episode->podcast->name
|
||||
) ?>" class="flex flex-col text-lg leading-tight text-white" title="<?= lang(
|
||||
'Episode.back_to_podcast'
|
||||
) ?>">
|
||||
<?= $episode->podcast->title ?>
|
||||
<span class="text-sm text-gray-300">
|
||||
@<?= $episode->podcast->name ?>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container flex-1 mx-auto">
|
||||
<nav class="flex items-center px-2 py-4">
|
||||
<?php if ($previousEpisode): ?>
|
||||
<a class="flex items-center text-xs leading-snug text-gray-600 hover:text-gray-900" href="<?= $previousEpisode->link ?>" title="<?= $previousEpisode->title ?>">
|
||||
<?= icon('arrow-left', 'mr-2') ?>
|
||||
<div class="flex flex-col">
|
||||
<?= $previousEpisode->season_number ==
|
||||
$episode->season_number
|
||||
? lang('Episode.previous_episode')
|
||||
: lang('Episode.previous_season') ?>
|
||||
<span class="w-40 font-semibold truncate"><?= $previousEpisode->title ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($nextEpisode): ?>
|
||||
<a class="flex items-center ml-auto text-xs leading-snug text-right text-gray-600 hover:text-gray-900" href="<?= $nextEpisode->link ?>" title="<?= $nextEpisode->title ?>">
|
||||
<div class="flex flex-col">
|
||||
<?= $nextEpisode->season_number ==
|
||||
$episode->season_number
|
||||
? lang('Episode.next_episode')
|
||||
: lang('Episode.next_season') ?>
|
||||
<span class="w-40 font-semibold truncate"><?= $nextEpisode->title ?></span>
|
||||
</div>
|
||||
<?= icon('arrow-right', 'ml-2') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<header class="flex flex-col items-center px-4 md:items-stretch md:justify-center md:flex-row">
|
||||
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full max-w-xs mb-2 rounded-lg md:mb-0 md:mr-4" />
|
||||
<div class="flex flex-col w-full max-w-sm">
|
||||
<h1 class="text-lg font-semibold md:text-2xl"><?= $episode->title ?></h1>
|
||||
<?php if ($episode->number): ?>
|
||||
<p class="text-gray-600">
|
||||
<?php if ($episode->season_number): ?>
|
||||
<a class="mr-1 underline hover:no-underline" href="<?= route_to(
|
||||
'podcast',
|
||||
$episode->podcast->name
|
||||
) .
|
||||
'?season=' .
|
||||
$episode->season_number ?>">
|
||||
<?= lang('Episode.season', [
|
||||
'seasonNumber' => $episode->season_number,
|
||||
]) ?></a>
|
||||
<?php endif; ?>
|
||||
<?= lang('Episode.number', [
|
||||
'episodeNumber' => $episode->number,
|
||||
]) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<div class="text-sm">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [$episode->published_at]) ?>
|
||||
</time>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang('Common.duration', [$episode->enclosure_duration]) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<a class="underline hover:no-underline" href="<?= route_to(
|
||||
'podcast',
|
||||
$podcast->name
|
||||
|
@ -27,3 +119,22 @@
|
|||
|
||||
<?= $this->endSection() ?>
|
||||
>>>>>>> 240f1d4... feat: enhance ui using javascript in admin area
|
||||
=======
|
||||
<section class="w-full max-w-3xl px-2 py-6 mx-auto prose md:px-6">
|
||||
<?= $episode->description_html ?>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="px-2 py-4 border-t ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row ">
|
||||
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
|
||||
<div class="flex flex-col items-end text-xs">
|
||||
<p><?= $episode->podcast->copyright ?></p>
|
||||
<p><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||
]) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
>>>>>>> ecc68b2... feat(public-ui): adapt wireframes to public podcast and episode pages
|
||||
|
|
|
@ -1,46 +1,157 @@
|
|||
<?= $this->extend('_layout') ?>
|
||||
<?= helper('page') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= $podcast->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= $podcast->language ?>">
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<header class="py-4 border-b">
|
||||
<h1 class="text-2xl"><?= $podcast->title ?></h1>
|
||||
<img src="<?= $podcast->image_url ?>" alt="Podcast cover" class="object-cover w-40 h-40 mb-6" />
|
||||
<a class="inline-flex px-4 py-2 bg-orange-500 hover:bg-orange-600" href="<?= route_to(
|
||||
'podcast_feed',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.feed') ?></a>
|
||||
</header>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title><?= $podcast->title ?></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="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
<?= $podcast->custom_html_head ?>
|
||||
</head>
|
||||
|
||||
<section class="flex flex-col py-4">
|
||||
<h2 class="mb-4 text-xl"><?= lang('Podcast.list_of_episodes') ?> (<?= count(
|
||||
$episodes
|
||||
) ?>)</h2>
|
||||
<?php if ($episodes): ?>
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<article class="flex w-full max-w-lg p-4 mb-4 border shadow">
|
||||
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<a href="<?= $episode->link ?>">
|
||||
<h3 class="text-xl font-semibold">
|
||||
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
|
||||
<span class="text-base font-bold text-gray-600">#<?= $episode->number ?></span>
|
||||
</h3>
|
||||
</a>
|
||||
<audio controls class="mt-auto" preload="none">
|
||||
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<main class="flex-1 bg-gray-200">
|
||||
<header class="border-b bg-gradient-to-tr from-gray-900 to-gray-800">
|
||||
<div class="flex flex-col items-center justify-center md:items-stretch md:mx-auto md:container md:py-12 md:flex-row ">
|
||||
<img src="<?= $podcast->image_url ?>" alt="Podcast cover" class="object-cover w-full max-w-xs m-4 rounded-lg shadow-xl" />
|
||||
<div class="w-full p-4 bg-white md:w-auto md:text-white md:bg-transparent">
|
||||
<h1 class="text-2xl font-semibold leading-tight"><?= $podcast->title ?> <span class="text-lg font-normal opacity-75">@<?= $podcast->name ?></span></h1>
|
||||
<div class="flex items-center mb-4">
|
||||
<address>
|
||||
<?= lang('Podcast.by', [
|
||||
'author' => $podcast->author,
|
||||
]) ?>
|
||||
</address>
|
||||
<?= $podcast->explicit
|
||||
? '<span class="px-1 ml-2 text-xs font-semibold leading-tight tracking-wider uppercase border-2 border-gray-700 rounded md:border-white">' .
|
||||
lang('Common.explicit') .
|
||||
'</span>'
|
||||
: '' ?>
|
||||
</div>
|
||||
<div class="inline-flex">
|
||||
<?= anchor(
|
||||
route_to('podcast_feed', $podcast->name),
|
||||
icon('rss', 'mr-2') . lang('Podcast.feed'),
|
||||
[
|
||||
'class' =>
|
||||
'text-white bg-gradient-to-r from-orange-400 to-red-500 hover:to-orange-500 hover:bg-orange-500 inline-flex items-center px-2 py-1 mb-2 font-semibold rounded-lg shadow-md hover:bg-orange-600',
|
||||
]
|
||||
) ?>
|
||||
<?php foreach ($podcast->platforms as $platform): ?>
|
||||
<a href="<?= $platform->link_url ?>" title="<?= $platform->label ?>" target="_blank" rel="noopener noreferrer" class="ml-2">
|
||||
<?= platform_icon($platform->icon_filename, 'h-8') ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mb-2 opacity-75">
|
||||
<?= $podcast->description_html ?>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-sm text-gray-700 bg-gray-200 rounded">
|
||||
<?= lang(
|
||||
'Podcast.category_options.' .
|
||||
$podcast->category->code
|
||||
) ?>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
=======
|
||||
<section class="flex flex-col">
|
||||
<nav class="inline-flex justify-center px-4 bg-gray-100 border-b">
|
||||
<?php foreach ($episodesNav as $link): ?>
|
||||
<?= anchor(
|
||||
$link['route'],
|
||||
$link['label'] .
|
||||
' (' .
|
||||
$link['number_of_episodes'] .
|
||||
')',
|
||||
[
|
||||
'class' =>
|
||||
'px-2 py-1 font-semibold ' .
|
||||
($link['is_active']
|
||||
? 'border-b-2 border-gray-600'
|
||||
: 'text-gray-600 hover:text-gray-900'),
|
||||
]
|
||||
) ?>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<div class="container py-6 mx-auto">
|
||||
<?php if ($episodes): ?>
|
||||
<h1 class="px-4 mb-2 text-xl text-center">
|
||||
<?php if ($activeQuery['type'] == 'year'): ?>
|
||||
<?= lang('Podcast.list_of_episodes_year', [
|
||||
'year' => $activeQuery['value'],
|
||||
]) ?> (<?= count($episodes) ?>)
|
||||
<?php elseif ($activeQuery['type'] == 'season'): ?>
|
||||
<?= lang('Podcast.list_of_episodes_season', [
|
||||
'seasonNumber' => $activeQuery['value'],
|
||||
]) ?> (<?= count($episodes) ?>)
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<article class="flex w-full max-w-lg p-4 mx-auto">
|
||||
<img loading="lazy" src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<a class="text-sm hover:underline" href="<?= $episode->link ?>">
|
||||
<h2 class="inline-flex justify-between w-full font-bold leading-none group">
|
||||
<span class="mr-1 group-hover:underline"><?= $episode->title ?></span>
|
||||
<span class="font-bold text-gray-600">#<?= $episode->number ?></span>
|
||||
</h2>
|
||||
</a>
|
||||
<div class="mb-2 text-xs">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
$episode->published_at,
|
||||
]) ?>
|
||||
</time>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang('Common.duration', [
|
||||
$episode->enclosure_duration,
|
||||
]) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<h1 class="px-4 mb-2 text-xl text-center"><?= lang(
|
||||
'Podcast.no_episode'
|
||||
) ?></h1>
|
||||
<p class="italic text-center"><?= lang(
|
||||
'Podcast.no_episode_hint'
|
||||
) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="px-2 py-4 border-t ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row ">
|
||||
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
|
||||
<div class="flex flex-col items-center text-xs md:items-end">
|
||||
<p><?= $podcast->copyright ?></p>
|
||||
<p><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||
]) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
>>>>>>> ecc68b2... feat(public-ui): adapt wireframes to public podcast and episode pages
|
||||
|
|
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
|
@ -25,55 +25,55 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"codemirror": "^5.55.0",
|
||||
"codemirror": "^5.57.0",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.5.0",
|
||||
"prosemirror-state": "^1.3.3",
|
||||
"prosemirror-view": "^1.15.2"
|
||||
"prosemirror-view": "^1.15.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.5",
|
||||
"@babel/core": "^7.11.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@commitlint/cli": "^9.0.1",
|
||||
"@commitlint/config-conventional": "^9.0.1",
|
||||
"@prettier/plugin-php": "^0.14.2",
|
||||
"@rollup/plugin-babel": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^14.0.0",
|
||||
"@commitlint/cli": "^9.1.2",
|
||||
"@commitlint/config-conventional": "^9.1.2",
|
||||
"@prettier/plugin-php": "^0.14.3",
|
||||
"@rollup/plugin-babel": "^5.2.0",
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-multi-entry": "^3.0.1",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@tailwindcss/custom-forms": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.2.0",
|
||||
"@types/codemirror": "0.0.97",
|
||||
"@types/prosemirror-markdown": "^1.0.3",
|
||||
"@types/prosemirror-view": "^1.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
||||
"@typescript-eslint/parser": "^3.7.0",
|
||||
"@types/prosemirror-view": "^1.15.1",
|
||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"cssnano": "^4.1.10",
|
||||
"cz-conventional-changelog": "^3.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.11",
|
||||
"lint-staged": "^10.2.13",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "2.0.5",
|
||||
"prettier": "2.1.1",
|
||||
"prettier-plugin-organize-imports": "^1.1.1",
|
||||
"rollup": "^2.23.0",
|
||||
"rollup": "^2.26.6",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"rollup-plugin-postcss": "^3.1.3",
|
||||
"rollup-plugin-terser": "^6.1.0",
|
||||
"rollup-plugin-postcss": "^3.1.6",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"stylelint": "^13.6.1",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"svgo": "^1.3.2",
|
||||
"tailwindcss": "^1.4.6",
|
||||
"typescript": "^3.9.7"
|
||||
"tailwindcss": "^1.7.5",
|
||||
"typescript": "^4.0.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
|
@ -5,7 +5,9 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {},
|
||||
variants: {
|
||||
textDecoration: ["responsive", "hover", "focus", "group-hover"],
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/custom-forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
|
|
Loading…
Reference in New Issue