';
/**
* 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(
'%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';
}
}