From 257befe9a7b9600ca1a54023eb9ae37b4d3e97c1 Mon Sep 17 00:00:00 2001 From: Forkless Date: Tue, 12 May 2026 01:00:08 +0200 Subject: [PATCH] feat(dashboard): add storage stats dashboard widget toggleable via Screen Options --- assets/css/dashboard.css | 110 +++++++++++++ includes/class-dashboard.php | 295 +++++++++++++++++++++++++++++++++++ includes/class-plugin.php | 5 + 3 files changed, 410 insertions(+) create mode 100644 assets/css/dashboard.css create mode 100644 includes/class-dashboard.php diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..d2667ea --- /dev/null +++ b/assets/css/dashboard.css @@ -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; +} diff --git a/includes/class-dashboard.php b/includes/class-dashboard.php new file mode 100644 index 0000000..6e316de --- /dev/null +++ b/includes/class-dashboard.php @@ -0,0 +1,295 @@ +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 ); + ?> + +
+ +

+ +
+ render_file_card( + __( 'MP3', 'piperless' ), + $file_stats['mp3'] ?? [ 'count' => 0, 'bytes' => 0 ], + '#008a20' + ); ?> + render_file_card( + __( 'Opus', 'piperless' ), + $file_stats['opus'] ?? [ 'count' => 0, 'bytes' => 0 ], + '#1565c0' + ); ?> + render_file_card( + __( 'WAV', 'piperless' ), + $file_stats['wav'] ?? [ 'count' => 0, 'bytes' => 0 ], + '#f56e28' + ); ?> +
+ + +
+
+ + + + + 0 ) : ?> + + + + +
+ +
+ + + + +
+
+
+ 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 ); + ?> +
+
+
+
+
+ + */ + 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' ]; + } +} diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 0a64e39..43ad65a 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -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 );