feat(media-folder-pro): add virtual folder system for WordPress media library

Custom WordPress plugin that replaces the default flat media library with
a structured folder view. Features: hierarchical folders via custom taxonomy,
sidebar folder tree, drag & drop, modal integration with Elementor/builders,
bulk assign, upload auto-assign, toast notifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 14:08:49 +01:00
parent 4ad3303b18
commit 5014b9108f
19 changed files with 3692 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
<?php
/**
* Plugin Name: Media Folder Pro
* Plugin URI: https://www.project-pro.pl
* Description: Strukturyzowana biblioteka mediów z wirtualnymi folderami (podkatalogami).
* Version: 0.1.0
* Author: Project Pro
* Author URI: https://www.project-pro.pl
* License: GPL-2.0-or-later
* Text Domain: media-folder-pro
* Requires at least: 6.0
* Requires PHP: 8.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'MFP_VERSION', '0.1.0' );
define( 'MFP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MFP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once MFP_PLUGIN_DIR . 'includes/class-taxonomy.php';
require_once MFP_PLUGIN_DIR . 'includes/class-ajax-handler.php';
require_once MFP_PLUGIN_DIR . 'includes/class-media-query.php';
final class Media_Folder_Pro {
private static ?self $instance = null;
private MFP_Taxonomy $taxonomy;
private MFP_Ajax_Handler $ajax;
private MFP_Media_Query $media_query;
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->taxonomy = new MFP_Taxonomy();
$this->ajax = new MFP_Ajax_Handler( $this->taxonomy );
$this->media_query = new MFP_Media_Query();
add_action( 'init', [ $this->taxonomy, 'register' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_modal_assets' ], 20 );
add_action( 'wp_enqueue_media', [ $this, 'enqueue_modal_on_media_load' ] );
add_action( 'admin_footer-upload.php', [ $this, 'render_folder_tree_container' ] );
$this->ajax->register_hooks();
$this->media_query->register();
}
/**
* Shared localization data for all JS scripts.
*/
private function get_mfp_data(): array {
return [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mfp_nonce' ),
'i18n' => [
'allMedia' => __( 'Wszystkie media', 'media-folder-pro' ),
'newFolder' => __( 'Nowy folder', 'media-folder-pro' ),
'folderName' => __( 'Nazwa folderu:', 'media-folder-pro' ),
'rename' => __( 'Zmień nazwę', 'media-folder-pro' ),
'delete' => __( 'Usuń', 'media-folder-pro' ),
'newSubfolder' => __( 'Nowy podfolder', 'media-folder-pro' ),
'confirmDelete' => __( 'Czy na pewno usunąć ten folder?', 'media-folder-pro' ),
'folderNotEmpty' => __( 'Folder nie jest pusty. Usuń najpierw zawartość.', 'media-folder-pro' ),
'error' => __( 'Wystąpił błąd. Spróbuj ponownie.', 'media-folder-pro' ),
'assignSuccess' => __( 'Media przypisane do folderu.', 'media-folder-pro' ),
'assignError' => __( 'Nie udało się przypisać mediów.', 'media-folder-pro' ),
'dropHere' => __( 'Upuść tutaj', 'media-folder-pro' ),
'uncategorized' => __( 'Bez folderu', 'media-folder-pro' ),
'moveToFolder' => __( 'Przenieś do folderu', 'media-folder-pro' ),
'removeFromFolder' => __( 'Usuń z folderu', 'media-folder-pro' ),
'noSelection' => __( 'Zaznacz media do przeniesienia', 'media-folder-pro' ),
'folderCreated' => __( 'Folder utworzony', 'media-folder-pro' ),
'folderRenamed' => __( 'Nazwa zmieniona', 'media-folder-pro' ),
'folderDeleted' => __( 'Folder usunięty', 'media-folder-pro' ),
'folderMoved' => __( 'Folder przeniesiony', 'media-folder-pro' ),
'emptyState' => __( 'Brak folderów', 'media-folder-pro' ),
'createFirst' => __( 'Utwórz pierwszy folder', 'media-folder-pro' ),
],
];
}
/**
* Ensure mfpData is localized (registers inline script if tree JS not loaded).
*/
private function ensure_mfp_data(): void {
if ( wp_script_is( 'media-folder-pro-tree', 'enqueued' ) ) {
return;
}
wp_register_script( 'media-folder-pro-tree', false );
wp_enqueue_script( 'media-folder-pro-tree' );
wp_localize_script( 'media-folder-pro-tree', 'mfpData', $this->get_mfp_data() );
}
/**
* Full assets for Media Library pages (upload.php, media-new.php).
*/
public function enqueue_admin_assets( string $hook ): void {
if ( ! in_array( $hook, [ 'upload.php', 'media-new.php' ], true ) ) {
return;
}
wp_enqueue_style(
'media-folder-pro-admin',
MFP_PLUGIN_URL . 'assets/css/admin.css',
[],
MFP_VERSION
);
wp_enqueue_script(
'media-folder-pro-tree',
MFP_PLUGIN_URL . 'assets/js/folder-tree.js',
[],
MFP_VERSION,
true
);
wp_enqueue_script(
'media-folder-pro-filter',
MFP_PLUGIN_URL . 'assets/js/media-filter.js',
[ 'media-views', 'media-folder-pro-tree' ],
MFP_VERSION,
true
);
wp_enqueue_script(
'media-folder-pro-modal',
MFP_PLUGIN_URL . 'assets/js/modal-integration.js',
[ 'media-views', 'media-folder-pro-tree' ],
MFP_VERSION,
true
);
wp_localize_script( 'media-folder-pro-tree', 'mfpData', $this->get_mfp_data() );
}
/**
* Modal integration for standard admin pages (post editor, etc.).
*/
public function enqueue_modal_assets( string $hook ): void {
if ( $hook === 'upload.php' || $hook === 'media-new.php' ) {
return;
}
if ( ! did_action( 'wp_enqueue_media' ) && ! wp_script_is( 'media-views', 'enqueued' ) ) {
return;
}
if ( wp_script_is( 'media-folder-pro-modal', 'enqueued' ) ) {
return;
}
wp_enqueue_style(
'media-folder-pro-admin',
MFP_PLUGIN_URL . 'assets/css/admin.css',
[],
MFP_VERSION
);
$this->ensure_mfp_data();
wp_enqueue_script(
'media-folder-pro-modal',
MFP_PLUGIN_URL . 'assets/js/modal-integration.js',
[ 'media-views', 'media-folder-pro-tree' ],
MFP_VERSION,
true
);
}
/**
* Hook into wp_enqueue_media — fires whenever any plugin/theme calls wp_enqueue_media().
* Covers: Elementor, WPBakery, Divi, ACF, and any builder using wp.media.
*/
public function enqueue_modal_on_media_load(): void {
if ( wp_script_is( 'media-folder-pro-modal', 'enqueued' ) ) {
return;
}
// Skip pages handled by enqueue_admin_assets — wp_enqueue_media() fires
// BEFORE admin_enqueue_scripts on upload.php, which would poison the
// tree script handle with a false (empty) registration.
$screen = get_current_screen();
if ( $screen && in_array( $screen->base, [ 'upload', 'media' ], true ) ) {
return;
}
wp_enqueue_style(
'media-folder-pro-admin',
MFP_PLUGIN_URL . 'assets/css/admin.css',
[],
MFP_VERSION
);
$this->ensure_mfp_data();
wp_enqueue_script(
'media-folder-pro-modal',
MFP_PLUGIN_URL . 'assets/js/modal-integration.js',
[ 'media-views', 'media-folder-pro-tree' ],
MFP_VERSION,
true
);
}
public function render_folder_tree_container(): void {
echo '<div id="mfp-folder-root"></div>';
}
}
add_action( 'plugins_loaded', [ 'Media_Folder_Pro', 'instance' ] );