R2 Media Manager. Sections: Status, Bulk Operations, * Activity Log. Also enqueues the JS asset on this page only. * Also owns the top-level "Peptide Repo" umbrella menu registration * (guarded so whichever plugin loads first registers it once). * Who: Hooked into `admin_menu` and `admin_enqueue_scripts`. * Depends: PR_R2_Endpoint::ENDPOINT, PR_R2_Log::get_recent(), PR_R2_Log::count_by_status(). * * @see includes/class-r2-ajax.php (AJAX handlers triggered by page buttons) * @see includes/class-r2-log.php (provides log rows for display) * @see assets/r2-manager.js (batch loop, progress bar, pause/resume, test-conn) * @see peptiderepo-r2-manager.php (bootstraps this class) */ if ( ! defined( 'ABSPATH' ) ) { exit; } class PR_R2_Admin_Page { /** Shared parent slug used by all peptiderepo plugins for the umbrella menu. */ const PARENT_SLUG = 'peptiderepo'; const MENU_SLUG = 'peptiderepo-r2-manager'; const CAPABILITY = 'manage_options'; public static function init(): void { add_action( 'admin_menu', [ __CLASS__, 'register_menu' ] ); add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] ); } public static function register_menu(): void { self::ensure_umbrella_menu(); add_submenu_page( self::PARENT_SLUG, __( 'R2 Media Manager', 'peptiderepo-r2' ), __( 'R2 Media Manager', 'peptiderepo-r2' ), self::CAPABILITY, self::MENU_SLUG, [ __CLASS__, 'render' ] ); } /** * Register the top-level "Peptide Repo" umbrella menu if not already registered. * Uses the global $menu array as the canonical registry — safe regardless of which * plugin loads first or how many plugins share the umbrella. * * @return void * Side effects: may call add_menu_page() and remove_submenu_page(). */ private static function ensure_umbrella_menu(): void { global $menu; foreach ( (array) $menu as $item ) { if ( isset( $item[2] ) && self::PARENT_SLUG === $item[2] ) { return; // Already registered by another plugin. } } add_menu_page( __( 'Peptide Repo', 'peptiderepo-r2' ), __( 'Peptide Repo', 'peptiderepo-r2' ), self::CAPABILITY, self::PARENT_SLUG, '__return_null', 'dashicons-database', 3 ); // WP auto-creates a submenu item matching the parent slug — remove the duplicate. remove_submenu_page( self::PARENT_SLUG, self::PARENT_SLUG ); } /** * Enqueue JS + localized data only on this plugin's settings page. * * @param string $hook Current admin hook suffix. * @return void * Side effects: wp_enqueue_script, wp_localize_script. */ public static function enqueue_assets( string $hook ): void { // Hook suffix for submenu under custom parent: {parent_slug}_page_{menu_slug} if ( self::PARENT_SLUG . '_page_' . self::MENU_SLUG !== $hook ) { return; } wp_enqueue_script( 'pr-r2-manager', PR_R2_PLUGIN_URL . 'assets/r2-manager.js', [ 'jquery' ], PR_R2_VERSION, true ); wp_localize_script( 'pr-r2-manager', 'prR2', [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonceMigrate' => wp_create_nonce( 'pr_r2_migrate' ), 'nonceRemove' => wp_create_nonce( 'pr_r2_remove_local' ), 'nonceTest' => wp_create_nonce( 'pr_r2_test' ), 'nonceClearLog' => wp_create_nonce( 'pr_r2_clear_log' ), 'batchSize' => 10, ] ); } /** * Render the full admin page. * * @return void * Side effects: outputs HTML. */ public static function render(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Permission denied.', 'peptiderepo-r2' ) ); } $s = self::get_status(); $logs = PR_R2_Log::get_recent( 20 ); $card = 'border:1px solid #ccd0d4;border-radius:4px;padding:16px;min-width:260px;max-width:320px;'; ?>

v

(%)
media.peptiderepo.com

created_at ); ?> operation ); ?> status ? '#c00' : ( 'skipped' === $row->status ? '#888' : '#090' ); printf( '%s', esc_attr( $colour ), esc_html( $row->status ) ); ?> file_path ) ); ?> error_msg ); ?>
get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit'" ); $offloaded_table = $wpdb->prefix . 'as3cf_items'; $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$offloaded_table}'" ) === $offloaded_table; $offloaded = 0; if ( $table_exists ) { $offloaded = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT source_id) FROM {$offloaded_table} WHERE source_type = 'media-library'" ); } $local_remaining = 0; if ( $table_exists && $offloaded > 0 ) { // WP Offload Media does NOT rewrite _wp_attached_file in the DB — it applies // s3auto:// at runtime via a filter. The only reliable check is physical disk // presence: build the absolute path from the upload base + raw meta value. $upload_base = wp_upload_dir()['basedir']; $offloaded_ids = $wpdb->get_col( "SELECT DISTINCT oi.source_id FROM {$offloaded_table} oi INNER JOIN {$wpdb->posts} p ON p.ID = oi.source_id WHERE oi.source_type = 'media-library' AND p.post_type = 'attachment'" ); foreach ( $offloaded_ids as $id ) { $rel = get_post_meta( (int) $id, '_wp_attached_file', true ); if ( $rel && file_exists( $upload_base . '/' . $rel ) ) { $local_remaining++; } } } $not_offloaded = max( 0, $total - $offloaded ); $pct = $total > 0 ? (int) round( ( $offloaded / $total ) * 100 ) : 0; return compact( 'total', 'offloaded', 'pct', 'not_offloaded', 'local_remaining' ); } }