mirror of
https://github.com/forkless/Piperless.git
synced 2026-08-19 01:35:45 +02:00
feat(dashboard): add storage stats dashboard widget toggleable via Screen Options
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Piperless Dashboard Widget Styles
|
||||
*
|
||||
* Card-based layout for the WordPress admin dashboard widget
|
||||
* showing audio storage stats at a glance.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
/* ── Dashboard container ───────────────────────────────────────────────── */
|
||||
.piperless-dashboard {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.piperless-dash-empty {
|
||||
color: #757575;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Stat cards grid ───────────────────────────────────────────────────── */
|
||||
.piperless-dash-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.piperless-dash-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-left: 4px solid var(--card-accent);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.piperless-dash-card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.piperless-dash-card-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #646970;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.piperless-dash-card-sub {
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── Info rows ─────────────────────────────────────────────────────────── */
|
||||
.piperless-dash-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.piperless-dash-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.piperless-dash-info-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.piperless-dash-info-text {
|
||||
flex: 1;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.piperless-dash-info-link {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: #2271b1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.piperless-dash-info-link:hover {
|
||||
color: #135e96;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Color highlights per info row ─────────────────────────────────────── */
|
||||
.piperless-dash-info-posts {
|
||||
border-left: 4px solid #dba617;
|
||||
}
|
||||
|
||||
.piperless-dash-info-storage {
|
||||
border-left: 4px solid #2271b1;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard widget — audio storage at a glance.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard widget.
|
||||
*
|
||||
* Registers a WordPress dashboard widget (togglable via Screen Options)
|
||||
* that shows:
|
||||
*
|
||||
* - Counts and total size of WAV, MP3, and Opus files in the cache
|
||||
* - Number of published posts without an audio transcript
|
||||
* - Available disk space on the server
|
||||
*
|
||||
* @since 1.1.4
|
||||
*/
|
||||
class Dashboard {
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/** @var Cache_Manager */
|
||||
private Cache_Manager $cache;
|
||||
|
||||
/** @var string Widget slug. */
|
||||
private string $widget_id = 'piperless_dashboard';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Logger $logger Logger.
|
||||
* @param Cache_Manager $cache Cache manager.
|
||||
*/
|
||||
public function __construct( Logger $logger, Cache_Manager $cache ) {
|
||||
$this->logger = $logger;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action( 'wp_dashboard_setup', [ $this, 'register_widget' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the dashboard widget.
|
||||
*/
|
||||
public function register_widget(): void {
|
||||
wp_add_dashboard_widget(
|
||||
$this->widget_id,
|
||||
__( 'Piperless — Audio Storage', 'piperless' ),
|
||||
[ $this, 'render' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the dashboard widget.
|
||||
*/
|
||||
public function render(): void {
|
||||
$file_stats = $this->get_file_stats();
|
||||
$without = $this->get_posts_without_audio();
|
||||
$disk_free = $this->get_disk_free();
|
||||
$total_files = ( $file_stats['mp3']['count'] ?? 0 )
|
||||
+ ( $file_stats['wav']['count'] ?? 0 )
|
||||
+ ( $file_stats['opus']['count'] ?? 0 );
|
||||
?>
|
||||
|
||||
<div class="piperless-dashboard">
|
||||
<?php if ( 0 === $total_files ) : ?>
|
||||
<p class="piperless-dash-empty"><?php esc_html_e( 'No audio files generated yet. Publish a post or generate audio from the editor sidebar.', 'piperless' ); ?></p>
|
||||
<?php else : ?>
|
||||
<div class="piperless-dash-grid">
|
||||
<?php $this->render_file_card(
|
||||
__( 'MP3', 'piperless' ),
|
||||
$file_stats['mp3'] ?? [ 'count' => 0, 'bytes' => 0 ],
|
||||
'#008a20'
|
||||
); ?>
|
||||
<?php $this->render_file_card(
|
||||
__( 'Opus', 'piperless' ),
|
||||
$file_stats['opus'] ?? [ 'count' => 0, 'bytes' => 0 ],
|
||||
'#1565c0'
|
||||
); ?>
|
||||
<?php $this->render_file_card(
|
||||
__( 'WAV', 'piperless' ),
|
||||
$file_stats['wav'] ?? [ 'count' => 0, 'bytes' => 0 ],
|
||||
'#f56e28'
|
||||
); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="piperless-dash-info">
|
||||
<div class="piperless-dash-info-item piperless-dash-info-posts">
|
||||
<span class="piperless-dash-info-icon">✎</span>
|
||||
<span class="piperless-dash-info-text">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of posts without audio */
|
||||
esc_html( _n(
|
||||
'%d post without an audio transcript.',
|
||||
'%d posts without an audio transcript.',
|
||||
$without,
|
||||
'piperless'
|
||||
) ),
|
||||
$without
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
<?php if ( $without > 0 ) : ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=post' ) ); ?>" class="piperless-dash-info-link">
|
||||
<?php esc_html_e( 'View posts', 'piperless' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="piperless-dash-info-item piperless-dash-info-storage">
|
||||
<span class="piperless-dash-info-icon">⌨</span>
|
||||
<span class="piperless-dash-info-text">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: human-readable free disk space */
|
||||
esc_html__( 'Server storage available: %s', 'piperless' ),
|
||||
esc_html( $disk_free )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single file-type stat card.
|
||||
*
|
||||
* @param string $label e.g. "MP3".
|
||||
* @param array $stats ['count' => int, 'bytes' => int].
|
||||
* @param string $color Accent color hex.
|
||||
*/
|
||||
private function render_file_card( string $label, array $stats, string $color ): void {
|
||||
$count = $stats['count'] ?? 0;
|
||||
$size = $this->human_size( $stats['bytes'] ?? 0 );
|
||||
?>
|
||||
<div class="piperless-dash-card" style="--card-accent: <?php echo esc_attr( $color ); ?>;">
|
||||
<div class="piperless-dash-card-value"><?php echo esc_html( (string) $count ); ?></div>
|
||||
<div class="piperless-dash-card-label"><?php echo esc_html( $label ); ?></div>
|
||||
<div class="piperless-dash-card-sub"><?php echo esc_html( $size ); ?></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the cache directory and return per-format file counts and byte totals.
|
||||
*
|
||||
* @return array<string,array{count:int,bytes:int}>
|
||||
*/
|
||||
private function get_file_stats(): array {
|
||||
$cache_dir = $this->cache->dir();
|
||||
$stats = [
|
||||
'mp3' => [ 'count' => 0, 'bytes' => 0 ],
|
||||
'wav' => [ 'count' => 0, 'bytes' => 0 ],
|
||||
'opus' => [ 'count' => 0, 'bytes' => 0 ],
|
||||
];
|
||||
|
||||
if ( ! is_dir( $cache_dir ) ) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
foreach ( [ 'mp3', 'wav', 'opus' ] as $ext ) {
|
||||
$files = (array) glob( $cache_dir . '/*.' . $ext );
|
||||
foreach ( $files as $file ) {
|
||||
$size = @filesize( $file );
|
||||
if ( false !== $size ) {
|
||||
$stats[ $ext ]['count']++;
|
||||
$stats[ $ext ]['bytes'] += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count published posts / pages without an audio transcript.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_posts_without_audio(): int {
|
||||
global $wpdb;
|
||||
|
||||
$types = $this->supported_post_types();
|
||||
$placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM {$wpdb->posts} p
|
||||
WHERE p.post_type IN ( {$placeholders} )
|
||||
AND p.post_status = 'publish'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM {$wpdb->postmeta} pm
|
||||
WHERE pm.post_id = p.ID
|
||||
AND pm.meta_key = '_piperless_audio_url'
|
||||
AND pm.meta_value != ''
|
||||
)",
|
||||
...$types
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable free disk space on the uploads directory volume.
|
||||
*
|
||||
* @return string e.g. "12.3 GB"
|
||||
*/
|
||||
private function get_disk_free(): string {
|
||||
$dir = $this->cache->dir();
|
||||
|
||||
// Walk up until we find a directory that exists.
|
||||
while ( ! is_dir( $dir ) && '/' !== $dir ) {
|
||||
$dir = dirname( $dir );
|
||||
}
|
||||
|
||||
$free = @disk_free_space( $dir );
|
||||
|
||||
if ( false === $free || null === $free ) {
|
||||
return __( 'Unavailable', 'piperless' );
|
||||
}
|
||||
|
||||
return $this->human_size( $free );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes as a human-readable string.
|
||||
*
|
||||
* @param int $bytes Raw byte count.
|
||||
* @return string e.g. "24.5 MB"
|
||||
*/
|
||||
private function human_size( int $bytes ): string {
|
||||
if ( 0 === $bytes ) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
|
||||
$exp = (int) floor( log( $bytes, 1024 ) );
|
||||
$exp = min( $exp, count( $units ) - 1 );
|
||||
|
||||
$val = $bytes / ( 1024 ** $exp );
|
||||
|
||||
return round( $val, 1 ) . ' ' . $units[ $exp ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue dashboard-specific styles on the dashboard page only.
|
||||
*
|
||||
* @param string $hook_suffix Current admin page hook.
|
||||
*/
|
||||
public function enqueue_styles( string $hook_suffix ): void {
|
||||
if ( 'index.php' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'piperless-dashboard',
|
||||
PIPERLESS_PLUGIN_URL . 'assets/css/dashboard.css',
|
||||
[],
|
||||
PIPERLESS_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported post types for audio transcripts.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function supported_post_types(): array {
|
||||
$types = apply_filters( 'piperless_post_types', [ 'post', 'page' ] );
|
||||
return is_array( $types ) ? $types : [ 'post' ];
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ class Plugin {
|
||||
/** @var Gutenberg */
|
||||
private Gutenberg $gutenberg;
|
||||
|
||||
/** @var Dashboard */
|
||||
private Dashboard $dashboard;
|
||||
|
||||
/**
|
||||
* Private constructor — use ::instance().
|
||||
*/
|
||||
@@ -68,6 +71,7 @@ class Plugin {
|
||||
$this->transcriber = new Transcriber( $this->logger, $this->piper, $this->cache_manager );
|
||||
$this->player = new Player( $this->logger );
|
||||
$this->gutenberg = new Gutenberg( $this->logger, $this->transcriber, $this->cache_manager );
|
||||
$this->dashboard = new Dashboard( $this->logger, $this->cache_manager );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +93,7 @@ class Plugin {
|
||||
$this->settings->init();
|
||||
$this->gutenberg->init();
|
||||
$this->player->init();
|
||||
$this->dashboard->init();
|
||||
|
||||
// Auto-generate on publish.
|
||||
add_action( 'transition_post_status', [ $this, 'maybe_auto_generate' ], 10, 3 );
|
||||
|
||||
Reference in New Issue
Block a user