'; /** * Warning SVG icon (20x20 viewBox). */ private const ICON_WARNING = ''; /** * Register the observer with WordPress hooks. */ public function register(): void { // Attachment edit form add_filter('attachment_fields_to_edit', [$this, 'addStatusField'], 10, 2); // Media library grid view (JS API) add_filter('wp_prepare_attachment_for_js', [$this, 'addOffloadFlagToAttachment'], 10, 2); // Assets add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); // Media library list view add_filter('manage_media_columns', [$this, 'addOffloadColumn']); add_action('manage_media_custom_column', [$this, 'renderOffloadColumn'], 10, 2); } /** * Add offload status field to attachment edit form. * * @param array $form_fields The form fields for the attachment. * @param \WP_Post $post The attachment post object. * @return array The modified form fields. */ public function addStatusField(array $form_fields, \WP_Post $post): array { $status = $this->getStatusDetails($post->ID); $form_fields['advmo_offload_status'] = [ 'label' => __('Offload Status', 'advanced-media-offloader'), 'input' => 'html', 'html' => $this->renderStatusBadge($status), ]; return $form_fields; } /** * Expose offload status flags to JavaScript attachment objects. * * @param array $response The attachment response array. * @param \WP_Post $attachment The attachment post object. * @return array The modified response. */ public function addOffloadFlagToAttachment(array $response, \WP_Post $attachment): array { if ($attachment->post_type === 'attachment') { $response['advmoOffloaded'] = $this->is_offloaded($attachment->ID); $response['advmoHasErrors'] = $this->has_errors($attachment->ID); } return $response; } /** * Add offload status column header to media list view. * * @param array $columns Existing columns. * @return array Modified columns. */ public function addOffloadColumn(array $columns): array { $new_columns = []; foreach ($columns as $key => $value) { $new_columns[$key] = $value; if ($key === 'title') { $new_columns['advmo_offload'] = sprintf( '%2$s%1$s', esc_attr__('Offload Status', 'advanced-media-offloader'), self::ICON_CLOUD ); } } return $new_columns; } /** * Render offload status column content in media list view. * * @param string $column_name The column name. * @param int $post_id The attachment post ID. */ public function renderOffloadColumn(string $column_name, int $post_id): void { if ($column_name !== 'advmo_offload') { return; } if ($this->is_offloaded($post_id)) { printf( '%s', esc_attr__('Offloaded', 'advanced-media-offloader'), self::ICON_CLOUD // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static SVG defined by the plugin. ); return; } if ($this->has_errors($post_id)) { printf( '%s', esc_attr__('Offload failed', 'advanced-media-offloader'), self::ICON_WARNING // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static SVG defined by the plugin. ); } } /** * Enqueue styles and scripts for media library and editor. * * @param mixed $hook The current admin page hook (may be empty in some contexts). */ public function enqueueAssets($hook = ''): void { // Ensure hook is a valid string (some page builders may not pass this parameter) if (!is_string($hook) || $hook === '') { return; } if (!$this->shouldEnqueueAssets($hook)) { return; } $this->enqueueStyles(); $this->enqueueScripts(); } /** * Determine if assets should be enqueued on the current screen. * * @param string $hook The current admin page hook. * @return bool True if assets should be loaded. */ private function shouldEnqueueAssets($hook): bool { // Media library (grid/list view) if ($hook === 'upload.php') { return true; } // Gutenberg editor (post/page/CPT edit screens) if (in_array($hook, ['post.php', 'post-new.php'], true)) { return $this->isBlockEditorActive(); } return false; } /** * Check if the block editor is active for the current post type. * * @return bool True if block editor is active. */ private function isBlockEditorActive(): bool { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen) { return false; } // WordPress 5.0+ block editor check if (method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) { return true; } // Fallback: check if Gutenberg is active for this post type $post_type = $screen->post_type ?? ''; if ($post_type && function_exists('use_block_editor_for_post_type')) { return use_block_editor_for_post_type($post_type); } return false; } /** * Get offload status details for an attachment. * * @param int $post_id The attachment post ID. * @return array{status: string, type: string} */ private function getStatusDetails(int $post_id): array { if ($this->is_offloaded($post_id)) { return [ 'status' => $this->buildOffloadedStatusMessage($post_id), 'type' => 'success', ]; } if ($this->has_errors($post_id)) { return [ 'status' => $this->buildErrorStatusMessage(), 'type' => 'error', ]; } return [ 'status' => __('Not offloaded yet', 'advanced-media-offloader'), 'type' => 'pending', ]; } /** * Build status message for successfully offloaded attachment. * * @param int $post_id The attachment post ID. * @return string The formatted status message. */ private function buildOffloadedStatusMessage(int $post_id): string { $provider = get_post_meta($post_id, self::META_PROVIDER, true); $bucket = get_post_meta($post_id, self::META_BUCKET, true); $offloaded_at = get_post_meta($post_id, self::META_OFFLOADED_AT, true); $message = sprintf( /* translators: %s: Cloud provider name */ __('Offloaded to %s', 'advanced-media-offloader'), esc_html($provider) ); if ($bucket) { $message .= sprintf( /* translators: %s: Bucket name */ __(' (Bucket: %s)', 'advanced-media-offloader'), esc_html($bucket) ); } if ($offloaded_at) { $date_format = get_option('date_format') . ' ' . get_option('time_format'); $formatted_date = date_i18n($date_format, $offloaded_at); $message .= sprintf( /* translators: %s: Date and time */ __(' on %s', 'advanced-media-offloader'), esc_html($formatted_date) ); } return $message; } /** * Build status message for failed offload with action link. * * @return string The formatted error message with link. */ private function buildErrorStatusMessage(): string { $overview_url = admin_url('admin.php?page=advmo_media_overview'); return sprintf( /* translators: %s: URL to Media Overview page */ __('Offload failed — View details', 'advanced-media-offloader'), esc_url($overview_url) ); } /** * Render the status badge HTML. * * @param array $status The status details array. * @return string The generated HTML. */ private function renderStatusBadge(array $status): string { return sprintf( '%s', esc_attr($status['type']), wp_kses($status['status'], ['a' => ['href' => []]]) ); } /** * Enqueue the offload status stylesheet. */ private function enqueueStyles(): void { wp_enqueue_style( 'advmo-offload-status', $this->getAssetUrl('css/offload-status.css'), [], $this->getAssetVersion('css/offload-status.css') ); } /** * Enqueue the offload status script. */ private function enqueueScripts(): void { wp_enqueue_script( 'advmo-offload-status', $this->getAssetUrl('js/offload-status.js'), ['jquery', 'media-views'], $this->getAssetVersion('js/offload-status.js'), true ); wp_localize_script('advmo-offload-status', 'advmoOffloadStatus', [ 'cloudIcon' => self::ICON_CLOUD, 'warningIcon' => self::ICON_WARNING, ]); } /** * Get the URL for an asset file. * * @param string $path Relative path to the asset. * @return string Full URL to the asset. */ private function getAssetUrl(string $path): string { return plugins_url('assets/' . $path, dirname(__DIR__, 2) . '/advanced-media-offloader.php'); } /** * Get the version for an asset file based on file modification time. * * @param string $path Relative path to the asset. * @return string Version string. */ private function getAssetVersion(string $path): string { $file = dirname(__DIR__, 2) . '/assets/' . $path; if (file_exists($file)) { return (string) filemtime($file); } return '1.0.0'; } }