'ayg-gallery',
'plural' => 'ayg-galleries',
'ajax' => false
) );
}
/**
* Define the table columns.
*
* @since 2.8.0
* @return array Column ID => label pairs.
*/
public function get_columns() {
return array(
'cb' => '',
'title' => __( 'Title', 'automatic-youtube-gallery' ),
'source' => __( 'Source', 'automatic-youtube-gallery' ),
'videos' => __( 'Videos', 'automatic-youtube-gallery' ),
'status' => __( 'Status', 'automatic-youtube-gallery' ),
'schedule' => __( 'Next Import', 'automatic-youtube-gallery' ),
'shortcode' => __( 'Shortcode', 'automatic-youtube-gallery' )
);
}
/**
* Define which columns are sortable and which database column they map to.
*
* @since 2.8.0
* @return array Column ID => [ db_column, is_default_sort ] pairs.
*/
protected function get_sortable_columns() {
return array(
'title' => array( 'title', false ),
'source' => array( 'source_type', false ),
'videos' => array( 'video_count', false )
);
}
/**
* Register available bulk actions.
*
* @since 2.8.0
* @return array Action key => label pairs.
*/
protected function get_bulk_actions() {
return array(
'delete' => __( 'Delete', 'automatic-youtube-gallery' )
);
}
/**
* Suppress the top and bottom navigation bars when there are no items.
*
* @since 2.8.0
* @param string $which 'top' or 'bottom'.
*/
protected function display_tablenav( $which ) {
if ( empty( $this->items ) ) {
return;
}
parent::display_tablenav( $which );
}
/**
* Output a message when the table is empty.
*
* @since 2.8.0
*/
public function no_items() {
esc_html_e( 'No galleries found.', 'automatic-youtube-gallery' );
}
/**
* Render the checkbox column used for bulk selection.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_cb( $item ) {
return sprintf( '', absint( $item->id ) );
}
/**
* Render the title column with an edit link and row actions (Edit, Delete).
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_title( $item ) {
$base_url = admin_url( 'admin.php?page=automatic-youtube-gallery' );
$edit_url = add_query_arg( array( 'action' => 'edit', 'id' => $item->id ), $base_url );
$title = sprintf(
'%s',
esc_url( $edit_url ),
esc_html( $item->title )
);
$actions = array(
'edit' => sprintf(
'%s',
esc_url( $edit_url ),
__( 'Edit', 'automatic-youtube-gallery' )
),
);
$actions['delete'] = sprintf(
'%s',
absint( $item->id ),
__( 'Delete', 'automatic-youtube-gallery' )
);
return $title . $this->row_actions( $actions );
}
/**
* Render the Source column showing the source type label.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_source( $item ) {
$type = ! empty( $item->source_type ) ? $item->source_type : 'playlist';
$label = ayg_get_source_type_label( $type );
return esc_html( $label );
}
/**
* Render the Videos column showing the denormalised video count.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_videos( $item ) {
// Live sources (search / livestream / single video) store nothing — they render on the fly —
// so a "0" count would be misleading. Show a dash instead.
if ( in_array( $item->source_type, array( 'search', 'livestream', 'video' ), true ) ) {
return '—';
}
return number_format_i18n( absint( $item->video_count ) );
}
/**
* Render the Status column as a badge.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_status( $item ) {
// Live sources (search / livestream / single video) never import — they query the API at
// display time — so the import-status labels ("Pending Import", etc.) don't apply. Show "Live".
if ( in_array( $item->source_type, array( 'search', 'livestream', 'video' ), true ) ) {
return '' . esc_html__( 'Live Source', 'automatic-youtube-gallery' ) . '';
}
$status = ! empty( $item->import_status ) ? $item->import_status : 'idle';
$params = json_decode( (string) $item->params, true );
$label = ayg_get_import_status_label( $status, $item->video_count, ! empty( $params['page_token'] ) );
return sprintf(
'%s',
esc_attr( $status ),
esc_html( $label )
);
}
/**
* Render the Schedule column: The next scheduled import date.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_schedule( $item ) {
if ( empty( $item->next_import_at ) ) {
return '—';
}
static $date_format = null;
if ( null === $date_format ) {
$date_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
}
return esc_html( wp_date( $date_format, strtotime( $item->next_import_at ) ) );
}
/**
* Render the Shortcode column as a one-click copy chip.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @return string
*/
protected function column_shortcode( $item ) {
$shortcode = sprintf( '[automatic_youtube_gallery id="%d"]', absint( $item->id ) );
return sprintf(
'',
esc_attr( $shortcode ),
esc_html__( 'Copy Shortcode', 'automatic-youtube-gallery' )
);
}
/**
* Fallback renderer for any column that does not have a dedicated method.
*
* @since 2.8.0
* @param object $item Gallery row object.
* @param string $column_name Column identifier.
* @return string
*/
public function column_default( $item, $column_name ) {
return '';
}
/**
* Query galleries from the database and populate $this->items with the current page.
*
* @since 2.8.0
*/
public function prepare_items() {
$this->_column_headers = array(
$this->get_columns(),
array(),
$this->get_sortable_columns(),
);
global $wpdb;
$table = $wpdb->prefix . 'ayg_galleries';
/**
* Filters the number of galleries listed per page in the admin list table.
*
* @since 2.8.0
* @param int $per_page Number of galleries per page. Default 20.
*/
$per_page = (int) apply_filters( 'ayg_galleries_per_page', 20 );
if ( $per_page < 1 ) {
$per_page = 20;
}
$curr_page = $this->get_pagenum();
$offset = ( $curr_page - 1 ) * $per_page;
$search = isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';
$allowed_orderby = array( 'id', 'title', 'source_type', 'video_count', 'last_imported_at' );
$orderby = ( isset( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], $allowed_orderby, true ) ) ? $_REQUEST['orderby'] : 'id';
$order = ( isset( $_REQUEST['order'] ) && 'asc' === strtolower( $_REQUEST['order'] ) ) ? 'ASC' : 'DESC';
if ( ! empty( $search ) ) {
$like = '%' . $wpdb->esc_like( $search ) . '%';
$total_items = (int) $wpdb->get_var(
$wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE `title` LIKE %s", $like )
);
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM `{$table}` WHERE `title` LIKE %s ORDER BY `{$orderby}` {$order} LIMIT %d OFFSET %d",
$like,
$per_page,
$offset
)
);
} else {
$total_items = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM `{$table}` ORDER BY `{$orderby}` {$order} LIMIT %d OFFSET %d",
$per_page,
$offset
)
);
}
$this->set_pagination_args( array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => (int) ceil( $total_items / $per_page )
) );
}
/**
* Process the bulk Delete action, then redirect back to the list with a count notice.
*
* @since 2.8.0
*/
public function process_bulk_action() {
if ( 'delete' !== $this->current_action() ) {
return;
}
check_admin_referer( 'bulk-ayg-galleries' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( -1 );
}
$ids = isset( $_REQUEST['gallery_ids'] ) ? array_map( 'absint', (array) $_REQUEST['gallery_ids'] ) : array();
if ( empty( $ids ) ) {
return;
}
foreach ( $ids as $id ) {
self::delete_gallery( $id );
}
$redirect_to = remove_query_arg(
array( 'action', 'action2', 'bulk_action', '_wpnonce', '_wp_http_referer', 'gallery_ids', 'deleted' )
);
wp_redirect( add_query_arg( 'deleted', count( $ids ), $redirect_to ) );
exit;
}
/**
* Delete a gallery, its pivot rows, and any orphaned videos.
*
* Orphaned videos are removed in 200-row chunks via LEFT JOIN to keep
* table locks short on large datasets.
*
* Called by process_bulk_action() and by AYG_Admin::ajax_callback_delete_gallery().
*
* @since 2.8.0
* @param int $id Gallery ID.
*/
public static function delete_gallery( $id ) {
global $wpdb;
$id = absint( $id );
if ( ! $id ) {
return;
}
$videos_table = $wpdb->prefix . 'ayg_videos';
$rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
$gal_table = $wpdb->prefix . 'ayg_galleries';
$wpdb->delete( $rel_table, array( 'gallery_id' => strval( $id ) ), array( '%s' ) );
$wpdb->delete( $gal_table, array( 'id' => $id ), array( '%d' ) );
do {
// Multi-table DELETE with LIMIT is not valid in MySQL/MariaDB.
// Use a derived subquery so the outer DELETE targets a single table by id.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$deleted = $wpdb->query(
"DELETE FROM `{$videos_table}`
WHERE `id` IN (
SELECT `id` FROM (
SELECT v.`id`
FROM `{$videos_table}` v
LEFT JOIN `{$rel_table}` r ON v.video_id = r.video_id
WHERE r.video_id IS NULL
LIMIT 200
) AS orphans
)"
);
} while ( $deleted > 0 );
}
}