mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(admin): add instance wide dashboard with storage and bandwidth usage
* add DashboardCard component * add instance wide podcasts and episodes numbers * add app.storageLimit environment variable * divide bytes by 1000 instead of 1024 in stats sql queries closes #216
This commit is contained in:
parent
3d363f2efe
commit
b1a6c02e56
@ -451,4 +451,9 @@ class App extends BaseConfig
|
||||
];
|
||||
|
||||
public string $theme = 'pine';
|
||||
|
||||
/**
|
||||
* Storage limit in Gigabytes
|
||||
*/
|
||||
public ?int $storageLimit = null;
|
||||
}
|
||||
|
@ -179,9 +179,9 @@ if (! function_exists('publication_button')) {
|
||||
break;
|
||||
}
|
||||
|
||||
return <<<CODE_SAMPLE
|
||||
return <<<HTML
|
||||
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button>
|
||||
CODE_SAMPLE;
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ if (! function_exists('publication_status_banner')) {
|
||||
case 'scheduled':
|
||||
$bannerDisclaimer = lang('Podcast.publication_status_banner.draft_mode');
|
||||
$bannerText = lang('Podcast.publication_status_banner.scheduled', [
|
||||
'publication_date' => local_time($publicationDate),
|
||||
'publication_date' => local_datetime($publicationDate),
|
||||
], null, false);
|
||||
$linkRoute = route_to('podcast-publish_edit', $podcastId);
|
||||
$linkLabel = lang('Podcast.publish_edit');
|
||||
@ -218,7 +218,7 @@ if (! function_exists('publication_status_banner')) {
|
||||
break;
|
||||
}
|
||||
|
||||
return <<<CODE_SAMPLE
|
||||
return <<<HTML
|
||||
<div class="flex items-center px-12 py-1 border-b bg-stripes-gray border-subtle" role="alert">
|
||||
<p class="text-gray-900">
|
||||
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
|
||||
@ -226,7 +226,7 @@ if (! function_exists('publication_status_banner')) {
|
||||
</p>
|
||||
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$linkLabel}</a>
|
||||
</div>
|
||||
CODE_SAMPLE;
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +321,7 @@ if (! function_exists('audio_player')) {
|
||||
$language = service('request')
|
||||
->getLocale();
|
||||
|
||||
return <<<CODE_SAMPLE
|
||||
return <<<HTML
|
||||
<vm-player
|
||||
id="castopod-vm-player"
|
||||
theme="light"
|
||||
@ -346,7 +346,7 @@ if (! function_exists('audio_player')) {
|
||||
</vm-controls>
|
||||
</vm-ui>
|
||||
</vm-player>
|
||||
CODE_SAMPLE;
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,16 +361,60 @@ if (! function_exists('relative_time')) {
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ISO8601);
|
||||
|
||||
return <<<CODE_SAMPLE
|
||||
return <<<HTML
|
||||
<time-ago class="{$class}" datetime="{$datetime}">
|
||||
<time
|
||||
datetime="{$datetime}"
|
||||
title="{$time}">{$translatedDate}</time>
|
||||
</time-ago>
|
||||
CODE_SAMPLE;
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('local_datetime')) {
|
||||
function local_datetime(Time $time): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ISO8601);
|
||||
|
||||
return <<<HTML
|
||||
<local-time datetime="{$datetime}"
|
||||
weekday="long"
|
||||
month="long"
|
||||
day="numeric"
|
||||
year="numeric"
|
||||
hour="numeric"
|
||||
minute="numeric">
|
||||
<time
|
||||
datetime="{$datetime}"
|
||||
title="{$time}">{$translatedDate}</time>
|
||||
</local-time>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('local_date')) {
|
||||
function local_date(Time $time): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
|
||||
return <<<HTML
|
||||
<time title="{$time}">{$translatedDate}</time>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('explicit_badge')) {
|
||||
@ -381,9 +425,9 @@ if (! function_exists('explicit_badge')) {
|
||||
}
|
||||
|
||||
$explicitLabel = lang('Common.explicit');
|
||||
return <<<CODE_SAMPLE
|
||||
return <<<HTML
|
||||
<span class="px-1 text-xs font-semibold leading-tight tracking-wider uppercase border md:border-white/50 {$class}">{$explicitLabel}</span>
|
||||
CODE_SAMPLE;
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ declare(strict_types=1);
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
if (! function_exists('get_browser_language')) {
|
||||
/**
|
||||
@ -281,41 +280,16 @@ if (! function_exists('format_bytes')) {
|
||||
/**
|
||||
* Adapted from https://stackoverflow.com/a/2510459
|
||||
*/
|
||||
function formatBytes(float $bytes, int $precision = 2): string
|
||||
function formatBytes(float $bytes, bool $is_binary = false, int $precision = 2): string
|
||||
{
|
||||
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
$units = $is_binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB'] : ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= pow(1024, $pow);
|
||||
$bytes /= pow($is_binary ? 1024 : 1000, $pow);
|
||||
|
||||
return round($bytes, $precision) . $units[$pow];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('local_time')) {
|
||||
function local_time(Time $time): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ISO8601);
|
||||
|
||||
return <<<CODE_SAMPLE
|
||||
<local-time datetime="{$datetime}"
|
||||
weekday="long"
|
||||
month="long"
|
||||
day="numeric"
|
||||
year="numeric"
|
||||
hour="numeric"
|
||||
minute="numeric">
|
||||
<time
|
||||
datetime="{$datetime}"
|
||||
title="{$time}">{$translatedDate}</time>
|
||||
</local-time>
|
||||
CODE_SAMPLE;
|
||||
}
|
||||
}
|
||||
|
6
app/Resources/icons/database.svg
Normal file
6
app/Resources/icons/database.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M21 9.5v3c0 2.485-4.03 4.5-9 4.5s-9-2.015-9-4.5v-3c0 2.485 4.03 4.5 9 4.5s9-2.015 9-4.5zm-18 5c0 2.485 4.03 4.5 9 4.5s9-2.015 9-4.5v3c0 2.485-4.03 4.5-9 4.5s-9-2.015-9-4.5v-3zm9-2.5c-4.97 0-9-2.015-9-4.5S7.03 3 12 3s9 2.015 9 4.5-4.03 4.5-9 4.5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 396 B |
6
app/Resources/icons/play-circle.svg
Normal file
6
app/Resources/icons/play-circle.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM10.622 8.415l4.879 3.252a.4.4 0 0 1 0 .666l-4.88 3.252a.4.4 0 0 1-.621-.332V8.747a.4.4 0 0 1 .622-.332z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 363 B |
46
app/Views/Components/DashboardCard.php
Normal file
46
app/Views/Components/DashboardCard.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use ViewComponents\Component;
|
||||
|
||||
class DashboardCard extends Component
|
||||
{
|
||||
protected ?string $href = null;
|
||||
|
||||
protected string $glyph;
|
||||
|
||||
protected string $title;
|
||||
|
||||
protected string $subtitle;
|
||||
|
||||
public function setSubtitle(string $value): void
|
||||
{
|
||||
$this->subtitle = html_entity_decode($value);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$glyph = icon($this->glyph, 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base');
|
||||
|
||||
if ($this->href !== null && $this->href !== '') {
|
||||
$chevronRight = icon('chevron-right');
|
||||
$viewLang = lang('Common.view');
|
||||
return <<<HTML
|
||||
<a href="{$this->href}" class="flex items-center justify-between w-full max-w-sm p-4 bg-elevated focus:ring-accent rounded-xl border-3 border-subtle group">
|
||||
<div class="flex items-start">{$glyph}<div class="flex flex-col ml-2"><div class="flex items-center"><span class="text-xs font-semibold leading-loose tracking-wider uppercase">{$this->title}</span><div class="inline-flex items-center ml-4 transition -translate-x-full group-hover:translate-x-0 group-focus:translate-x-0"><span class="-ml-2 text-xs lowercase transition opacity-0 group-hover:opacity-100 group-focus:opacity-100">{$viewLang}</span>{$chevronRight}</div></div><p class="text-xs">{$this->subtitle}</p></div></div>
|
||||
<div class="mx-2 text-5xl font-bold">{$this->slot}</div>
|
||||
</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex items-center justify-between w-full max-w-sm p-4 bg-elevated rounded-xl border-3 border-subtle">
|
||||
<div class="flex items-start">{$glyph}<div class="flex flex-col ml-2"><span class="text-xs font-semibold leading-loose tracking-wider uppercase">{$this->title}</span><p class="text-xs">{$this->subtitle}</p></div></div>
|
||||
<div class="mx-2 text-5xl font-bold">{$this->slot}</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
1
ecs.php
1
ecs.php
@ -26,6 +26,7 @@ return static function (ECSConfig $ecsConfig): void {
|
||||
__DIR__ . '/app/Views/Components/*',
|
||||
__DIR__ . '/modules/**/Views/Components/*',
|
||||
__DIR__ . '/themes/**/Views/Components/*',
|
||||
__DIR__ . '/app/Helpers/components_helper.php'
|
||||
],
|
||||
|
||||
LineLengthFixer::class => [
|
||||
|
@ -19,7 +19,7 @@ $routes->group(
|
||||
'namespace' => 'Modules\Admin\Controllers',
|
||||
],
|
||||
function ($routes): void {
|
||||
$routes->get('/', 'HomeController', [
|
||||
$routes->get('/', 'DashboardController', [
|
||||
'as' => 'admin',
|
||||
]);
|
||||
|
||||
|
82
modules/Admin/Controllers/DashboardController.php
Normal file
82
modules/Admin/Controllers/DashboardController.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\MediaModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class DashboardController extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$podcastsData = [];
|
||||
$podcastsCount = (new PodcastModel())->builder()
|
||||
->countAll();
|
||||
$podcastsLastPublishedAt = (new PodcastModel())->builder()
|
||||
->select('MAX(published_at) as last_published_at')
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
->get()
|
||||
->getResultArray()[0]['last_published_at'];
|
||||
$podcastsData['number_of_podcasts'] = (int) $podcastsCount;
|
||||
$podcastsData['last_published_at'] = $podcastsLastPublishedAt === null ? null : new Time(
|
||||
$podcastsLastPublishedAt
|
||||
);
|
||||
|
||||
$episodesData = [];
|
||||
$episodesCount = (new EpisodeModel())->builder()
|
||||
->countAll();
|
||||
$episodesLastPublishedAt = (new EpisodeModel())->builder()
|
||||
->select('MAX(published_at) as last_published_at')
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
->get()
|
||||
->getResultArray()[0]['last_published_at'];
|
||||
$episodesData['number_of_episodes'] = (int) $episodesCount;
|
||||
$episodesData['last_published_at'] = $episodesLastPublishedAt === null ? null : new Time(
|
||||
$episodesLastPublishedAt
|
||||
);
|
||||
|
||||
$totalUploaded = (new MediaModel())->builder()
|
||||
->selectSum('file_size')
|
||||
->get()
|
||||
->getResultArray()[0];
|
||||
|
||||
$appStorageLimit = config('App')
|
||||
->storageLimit;
|
||||
if ($appStorageLimit === null || $appStorageLimit < 0) {
|
||||
$storageLimitBytes = disk_free_space('./');
|
||||
} else {
|
||||
$storageLimitBytes = $appStorageLimit * 1000000000;
|
||||
}
|
||||
|
||||
$storageData = [
|
||||
'limit' => formatBytes((int) $storageLimitBytes),
|
||||
'percentage' => round((((int) $totalUploaded['file_size']) / $storageLimitBytes) * 100, 0),
|
||||
'total_uploaded' => formatBytes((int) $totalUploaded['file_size']),
|
||||
];
|
||||
|
||||
$onlyPodcastId = null;
|
||||
if ($podcastsData['number_of_podcasts'] === 1) {
|
||||
$onlyPodcastId = (new PodcastModel())->first()
|
||||
->id;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'podcastsData' => $podcastsData,
|
||||
'episodesData' => $episodesData,
|
||||
'storageData' => $storageData,
|
||||
'onlyPodcastId' => $onlyPodcastId,
|
||||
];
|
||||
|
||||
return view('dashboard', $data);
|
||||
}
|
||||
}
|
@ -1,22 +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 Modules\Admin\Controllers;
|
||||
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
|
||||
class HomeController extends BaseController
|
||||
{
|
||||
public function index(): RedirectResponse
|
||||
{
|
||||
session()->keepFlashdata('message');
|
||||
return redirect()->route('podcast-list');
|
||||
}
|
||||
}
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'اختر أسلوب التفاعل',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'لوحة التحكم الإدارية',
|
||||
'home' => 'لوحة التحكم الإدارية',
|
||||
'welcome_message' => 'أهلًا بك في المنطقة الإدارية!',
|
||||
'choose_interact' => 'اختر أسلوب التفاعل',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'O lenn',
|
||||
],
|
||||
'size_limit' => 'Bevenn ar vent: {0}.',
|
||||
'choose_interact' => 'Dibabit penaos interaktiñ',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Taolenn-stur',
|
||||
'home' => 'Taolenn-stur',
|
||||
'welcome_message' => 'Degemer mat en daolenn-stur!',
|
||||
'choose_interact' => 'Dibabit penaos interaktiñ',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Spielt',
|
||||
],
|
||||
'size_limit' => 'Größenlimit: {0}.',
|
||||
'choose_interact' => 'Mit welchem Podcast-Profil wollen Sie handeln',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Adminübersicht',
|
||||
'home' => 'Adminübersicht',
|
||||
'welcome_message' => 'Willkommen im Administrationsbereich!',
|
||||
'choose_interact' => 'Mit welchem Podcast-Profil wollen Sie handeln',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Αναπαράγεται',
|
||||
],
|
||||
'size_limit' => 'Όριο μεγέθους: {0}.',
|
||||
'choose_interact' => 'Επιλέξτε τον τρόπο αλληλεπίδρασης',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Πίνακας ελέγχου διαχειριστή',
|
||||
'home' => 'Πίνακας ελέγχου διαχειριστή',
|
||||
'welcome_message' => 'Καλώς ήρθατε στην περιοχή διαχείρισης!',
|
||||
'choose_interact' => 'Επιλέξτε τον τρόπο αλληλεπίδρασης',
|
||||
];
|
@ -35,4 +35,6 @@ return [
|
||||
'by_weekday' => 'By week day (for the past 60 days)',
|
||||
'by_hour' => 'By time of day (for the past 60 days)',
|
||||
'podcast_by_bandwidth' => 'Daily used bandwidth (in MB)',
|
||||
'total_storage_by_month' => 'Monthly storage (in MB)',
|
||||
'total_bandwidth_by_month' => 'Monthly used bandwidth (in MB)',
|
||||
];
|
||||
|
@ -46,4 +46,6 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
'view' => 'View',
|
||||
];
|
||||
|
28
modules/Admin/Language/en/Dashboard.php
Normal file
28
modules/Admin/Language/en/Dashboard.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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/
|
||||
*/
|
||||
|
||||
return [
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'podcasts' => [
|
||||
'title' => 'Podcasts',
|
||||
'not_found' => 'No published podcast',
|
||||
'last_published' => 'Last published on {lastPublicationDate}',
|
||||
],
|
||||
'episodes' => [
|
||||
'title' => 'Episodes',
|
||||
'not_found' => 'No published episode',
|
||||
'last_published' => 'Last published on {lastPublicationDate}',
|
||||
],
|
||||
'storage' => [
|
||||
'title' => 'Storage',
|
||||
'subtitle' => '{totalUploaded} out of {totalStorage}',
|
||||
],
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Reproduciendo',
|
||||
],
|
||||
'size_limit' => 'Límite de tamaño: {0}.',
|
||||
'choose_interact' => 'Elige cómo interactuar',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Panel de administración',
|
||||
'home' => 'Panel de administración',
|
||||
'welcome_message' => '¡Bienvenido al área de administración!',
|
||||
'choose_interact' => 'Elige cómo interactuar',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'En cours',
|
||||
],
|
||||
'size_limit' => 'Taille maximale : {0}.',
|
||||
'choose_interact' => 'Choisissez comment interagir',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Tableau de bord',
|
||||
'home' => 'Tableau de bord',
|
||||
'welcome_message' => 'Bienvenue dans l’administration !',
|
||||
'choose_interact' => 'Choisissez comment interagir',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Wordt afgespeeld',
|
||||
],
|
||||
'size_limit' => 'Maximale grootte: {0}.',
|
||||
'choose_interact' => 'Kies hoe de interactie moet worden',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Beheerder overzicht',
|
||||
'home' => 'Beheerder overzicht',
|
||||
'welcome_message' => 'Welkom bij de beheerder omgeving!',
|
||||
'choose_interact' => 'Kies hoe de interactie moet worden',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Spelar',
|
||||
],
|
||||
'size_limit' => 'Maks storleik: {0}.',
|
||||
'choose_interact' => 'Vel korleis du vil samhandla',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Styringspanel',
|
||||
'home' => 'Styringspanel',
|
||||
'welcome_message' => 'Velkomen til styrarområdet!',
|
||||
'choose_interact' => 'Vel korleis du vil samhandla',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Odtwarzanie',
|
||||
],
|
||||
'size_limit' => 'Limit rozmiaru: {0}.',
|
||||
'choose_interact' => 'Wybierz sposób interakcji',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Pulpit administratora',
|
||||
'home' => 'Pulpit administratora',
|
||||
'welcome_message' => 'Witamy w panelu administracyjnym!',
|
||||
'choose_interact' => 'Wybierz sposób interakcji',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Reproduzindo',
|
||||
],
|
||||
'size_limit' => 'Limite de tamanho: {0}.',
|
||||
'choose_interact' => 'Escolha como interagir',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Painel de administração',
|
||||
'home' => 'Painel de administração',
|
||||
'welcome_message' => 'Bem-vindo à área de administração!',
|
||||
'choose_interact' => 'Escolha como interagir',
|
||||
];
|
@ -1,15 +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/
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Выберите как взаимодействовать',
|
||||
];
|
||||
|
@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Панель Администратора',
|
||||
'home' => 'Панель Администратора',
|
||||
'welcome_message' => 'Добро пожаловать в панель администрирования!',
|
||||
'choose_interact' => 'Выберите как взаимодействовать',
|
||||
];
|
@ -1,15 +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/
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
@ -46,4 +46,5 @@ return [
|
||||
'playing' => 'Playing',
|
||||
],
|
||||
'size_limit' => 'Size limit: {0}.',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
14
modules/Admin/Language/sv/Dashboard.php
Normal file
14
modules/Admin/Language/sv/Dashboard.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?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/
|
||||
*/
|
||||
|
||||
return [
|
||||
'home' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
];
|
@ -19,7 +19,7 @@ $routes->addPlaceholder(
|
||||
);
|
||||
$routes->addPlaceholder(
|
||||
'filter',
|
||||
'\bWeekly|\bYearly|\bByDay|\bByWeekday|\bByMonth|\bByAppWeekly|\bByAppYearly|\bByOsWeekly|\bByDeviceWeekly|\bBots|\bByServiceWeekly|\bBandwidthByDay|\bUniqueListenersByDay|\bUniqueListenersByMonth|\bTotalListeningTimeByDay|\bTotalListeningTimeByMonth|\bByDomainWeekly|\bByDomainYearly',
|
||||
'\bWeekly|\bYearly|\bByDay|\bByWeekday|\bByMonth|\bByAppWeekly|\bByAppYearly|\bByOsWeekly|\bByDeviceWeekly|\bBots|\bByServiceWeekly|\bBandwidthByDay|\bUniqueListenersByDay|\bUniqueListenersByMonth|\bTotalListeningTimeByDay|\bTotalListeningTimeByMonth|\bByDomainWeekly|\bByDomainYearly|\bTotalBandwidthByMonth|\bTotalStorageByMonth',
|
||||
);
|
||||
|
||||
$routes->group('', [
|
||||
@ -53,6 +53,10 @@ $routes->group('', [
|
||||
);
|
||||
});
|
||||
|
||||
$routes->get(config('Analytics')->gateway . '/(:class)/(:filter)', 'AnalyticsController::getData/$1/$2', [
|
||||
'as' => 'analytics-data-instance',
|
||||
]);
|
||||
|
||||
// Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
|
||||
$routes->head(
|
||||
'audio/(:base64)/(:any)',
|
||||
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Analytics\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
@ -17,6 +18,8 @@ use CodeIgniter\Model;
|
||||
|
||||
class AnalyticsController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected Model $analyticsModel;
|
||||
|
||||
protected string $methodName = '';
|
||||
@ -27,6 +30,12 @@ class AnalyticsController extends Controller
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if (! is_numeric($params[0])) {
|
||||
$this->analyticsModel = model('Analytics' . $params[0] . 'Model');
|
||||
$this->methodName = 'getData' . $params[1];
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
$this->analyticsModel = model('Analytics' . $params[1] . 'Model');
|
||||
$this->methodName = 'getData' . (count($params) >= 3 ? $params[2] : '');
|
||||
|
||||
@ -36,14 +45,18 @@ class AnalyticsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function getData(int $podcastId, ?int $episodeId = null): ResponseInterface
|
||||
public function getData(?int $podcastId = null, ?int $episodeId = null): ResponseInterface
|
||||
{
|
||||
$methodName = $this->methodName;
|
||||
|
||||
if ($episodeId === null) {
|
||||
return $this->response->setJSON($this->analyticsModel->{$methodName}($podcastId));
|
||||
if ($podcastId === null) {
|
||||
return $this->respond($this->analyticsModel->{$methodName}());
|
||||
}
|
||||
|
||||
return $this->response->setJSON($this->analyticsModel->{$methodName}($podcastId, $episodeId));
|
||||
if ($episodeId === null) {
|
||||
return $this->respond($this->analyticsModel->{$methodName}($podcastId));
|
||||
}
|
||||
|
||||
return $this->respond($this->analyticsModel->{$methodName}($podcastId, $episodeId));
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Analytics\Models;
|
||||
|
||||
use App\Entities\Media\BaseMedia;
|
||||
use App\Models\MediaModel;
|
||||
use CodeIgniter\Model;
|
||||
use Modules\Analytics\Entities\AnalyticsPodcasts;
|
||||
|
||||
@ -93,7 +95,7 @@ class AnalyticsPodcastModel extends Model
|
||||
public function getDataBandwidthByDay(int $podcastId): array
|
||||
{
|
||||
if (! ($found = cache("{$podcastId}_analytics_podcast_by_bandwidth"))) {
|
||||
$found = $this->select('date as labels, round(bandwidth / 1048576, 1) as `values`')
|
||||
$found = $this->select('date as labels, ROUND(bandwidth / 1000000, 2) as `values`')
|
||||
->where([
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
@ -235,4 +237,48 @@ class AnalyticsPodcastModel extends Model
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets total bandwidth data for instance
|
||||
*
|
||||
* @return AnalyticsPodcasts[]
|
||||
*/
|
||||
public function getDataTotalBandwidthByMonth(): array
|
||||
{
|
||||
if (! ($found = cache('analytics_total_bandwidth_by_month'))) {
|
||||
$found = $this->select(
|
||||
'DATE_FORMAT(updated_at,"%Y-%m") as labels, ROUND(sum(bandwidth) / 1000000, 2) as `values`'
|
||||
)
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()
|
||||
->save('analytics_total_bandwidth_by_month', $found, 600);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total storage
|
||||
*
|
||||
* @return BaseMedia[]
|
||||
*/
|
||||
public function getDataTotalStorageByMonth(): array
|
||||
{
|
||||
if (! ($found = cache('analytics_total_storage_by_month'))) {
|
||||
$found = (new MediaModel())->select(
|
||||
'DATE_FORMAT(uploaded_at,"%Y-%m") as labels, ROUND(sum(file_size) / 1000000, 2) as `values`'
|
||||
)
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()
|
||||
->save('analytics_total_storage_by_month', $found, 600);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
CODE_SAMPLE;
|
||||
}
|
||||
|
||||
$interactAsText = lang('Admin.choose_interact');
|
||||
$interactAsText = lang('Common.choose_interact');
|
||||
$route = route_to('interact-as-actor');
|
||||
$csrfField = csrf_field();
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
$navigation = [
|
||||
'dashboard' => [
|
||||
'icon' => 'dashboard',
|
||||
'items' => ['admin'],
|
||||
],
|
||||
'podcasts' => [
|
||||
'icon' => 'mic',
|
||||
'items' => ['podcast-list', 'podcast-create', 'podcast-import'],
|
||||
|
@ -2,13 +2,42 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Admin.dashboard') ?>
|
||||
<?= lang('Dashboard.home') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Admin.dashboard') ?>
|
||||
<?= lang('Dashboard.home') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<?= lang('Admin.welcome_message') ?>
|
||||
|
||||
<div class="flex flex-wrap items-start gap-4">
|
||||
<DashboardCard href="<?= route_to('podcast-list') ?>" glyph="mic" title="<?= lang('Dashboard.podcasts.title') ?>" subtitle="<?= $podcastsData['last_published_at'] ? esc(lang('Dashboard.podcasts.last_published', [
|
||||
'lastPublicationDate' => local_date($podcastsData['last_published_at']),
|
||||
], null, false)) : lang('Dashboard.podcasts.not_found') ?>"><?= $podcastsData['number_of_podcasts'] ?></DashboardCard>
|
||||
<DashboardCard href="<?= $onlyPodcastId === null ? '' : route_to('episode-list', $onlyPodcastId) ?>" glyph="play" title="<?= lang('Dashboard.episodes.title') ?>" subtitle="<?= $episodesData['last_published_at'] ? esc(lang('Dashboard.episodes.last_published', [
|
||||
'lastPublicationDate' => local_date($episodesData['last_published_at']),
|
||||
], null, false)) : lang('Dashboard.episodes.not_found') ?>"><?= $episodesData['number_of_episodes'] ?></DashboardCard>
|
||||
<DashboardCard glyph="database" title="<?= lang('Dashboard.storage.title') ?>" subtitle="<?= lang('Dashboard.storage.subtitle', [
|
||||
'totalUploaded' => $storageData['total_uploaded'],
|
||||
'totalStorage' => $storageData['limit'],
|
||||
]) ?>"><?= $storageData['percentage'] ?>%</DashboardCard>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 mt-4 lg:grid-cols-2">
|
||||
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_storage_by_month') ?>" dataUrl="<?= route_to(
|
||||
'analytics-data-instance',
|
||||
'Podcast',
|
||||
'TotalStorageByMonth',
|
||||
) ?>" />
|
||||
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_bandwidth_by_month') ?>" dataUrl="<?= route_to(
|
||||
'analytics-data-instance',
|
||||
'Podcast',
|
||||
'TotalBandwidthByMonth',
|
||||
) ?>" />
|
||||
</div>
|
||||
|
||||
|
||||
<?= service('vite')
|
||||
->asset('js/charts.ts', 'js') ?>
|
||||
<?= $this->endsection() ?>
|
||||
|
@ -21,12 +21,12 @@
|
||||
name="audio_file"
|
||||
label="<?= lang('Episode.form.audio_file') ?>"
|
||||
hint="<?= lang('Episode.form.audio_file_hint') ?>"
|
||||
helper="<?= lang('Common.size_limit', [formatBytes(file_upload_max_size())]) ?>"
|
||||
helper="<?= lang('Common.size_limit', [formatBytes(file_upload_max_size(), true)]) ?>"
|
||||
type="file"
|
||||
accept=".mp3,.m4a"
|
||||
required="true"
|
||||
data-max-size="<?= file_upload_max_size() ?>"
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size())]) ?>" />
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
|
||||
|
||||
<Forms.Field
|
||||
name="cover"
|
||||
|
@ -25,11 +25,11 @@
|
||||
name="audio_file"
|
||||
label="<?= lang('Episode.form.audio_file') ?>"
|
||||
hint="<?= lang('Episode.form.audio_file_hint') ?>"
|
||||
helper="<?= lang('Common.size_limit', [formatBytes(file_upload_max_size())]) ?>"
|
||||
helper="<?= lang('Common.size_limit', [formatBytes(file_upload_max_size(), true)]) ?>"
|
||||
type="file"
|
||||
accept=".mp3,.m4a"
|
||||
data-max-size="<?= file_upload_max_size() ?>"
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size())]) ?>" />
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
|
||||
|
||||
<Forms.Field
|
||||
name="cover"
|
||||
|
@ -6,7 +6,7 @@ $podcastNavigation = [
|
||||
'items' => ['podcast-view', 'podcast-edit', 'podcast-persons-manage'],
|
||||
],
|
||||
'episodes' => [
|
||||
'icon' => 'mic',
|
||||
'icon' => 'play-circle',
|
||||
'items' => ['episode-list', 'episode-create'],
|
||||
],
|
||||
'analytics' => [
|
||||
|
@ -37,7 +37,7 @@
|
||||
CODE_SAMPLE;
|
||||
}
|
||||
|
||||
$interactAsText = lang('Admin.choose_interact');
|
||||
$interactAsText = lang('Common.choose_interact');
|
||||
$route = route_to('interact-as-actor');
|
||||
$csrfField = csrf_field();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user